[CalendarServer-changes] [15120] CalendarServer/branches/users/sagen/clientsim

source_changes at macosforge.org source_changes at macosforge.org
Thu Sep 10 13:25:07 PDT 2015


Revision: 15120
          http://trac.calendarserver.org//changeset/15120
Author:   sagen at apple.com
Date:     2015-09-10 13:25:07 -0700 (Thu, 10 Sep 2015)
Log Message:
-----------
Checkpoint of sim

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/clientsim/calendarserver/push/amppush.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ampsim.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ical.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/logger.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/population.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/profiles.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sim.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/event-updates-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/events-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-accepts.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only-recurring.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/subscribe.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/webadmin.py

Added Paths:
-----------
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/debug.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/distributions.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/pubsub.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/push.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/records.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/Profile
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/StartupProfile
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_depth1_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendarhome_depth1_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_notification_depth1_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/post_freebusy.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/principal_search_report.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/report_principal_search.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_color_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_description_proppatch.request.xml
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_displayname_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_order_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_timezone_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_transparent_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_date_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_datetime_proppatch.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_create_calendar.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_delegate_principal_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_expand.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_initial_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principals_report.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_query_events_depth1_report.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_well_known_propfind.request
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/requester.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/resources.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sandbox.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/README.md
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/__init__.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/calendars-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients-old.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.dist.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/demo-clients.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/event-updates-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/events-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-accepts.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only-recurring.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/clients.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config-dist.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/defaults.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/preset_distributions.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/templates.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/__init__.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_distributions.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_ical.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_logger.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_population.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_profiles.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_pubsub.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_push.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_records.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_requester.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_resources.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_sim.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_templates.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_trafficlogger.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_webadmin.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/www/
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/www/layout.html

Removed Paths:
-------------
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.dist.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.plist
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_ical.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_population.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_profiles.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_sim.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_trafficlogger.py
    CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_webadmin.py

Modified: CalendarServer/branches/users/sagen/clientsim/calendarserver/push/amppush.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/calendarserver/push/amppush.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/calendarserver/push/amppush.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -42,7 +42,7 @@
 
 
 class UnsubscribeFromID(amp.Command):
-    arguments = [('token', amp.String()), ('id', amp.String())]
+    arguments = [('id', amp.String())]
     response = [('status', amp.String())]
 
 
@@ -257,7 +257,7 @@
         return {"status" : "OK"}
     SubscribeToID.responder(subscribe)
 
-    def unsubscribe(self, token, id):
+    def unsubscribe(self, id):
         try:
             del self.subscriptions[id]
         except KeyError:

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ampsim.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ampsim.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ampsim.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -46,7 +46,6 @@
             exit(0)
     runmain()
 
-
 from copy import deepcopy
 
 from plistlib import writePlistToString, readPlistFromString
@@ -56,15 +55,18 @@
 
 from twext.enterprise.adbapi2 import Pickle
 
-from contrib.performance.loadtest.sim import _DirectoryRecord, LoadSimulator
+from contrib.performance.loadtest.sim import LoadSimulator
+from contrib.performance.loadtest.records import DirectoryRecord
+from contrib.performance.loadtest.config import Config
 
 class Configure(Command):
     """
     Configure this worker process with the text of an XML property list.
     """
-    arguments = [("plist", String())]
+    arguments = [("cfg", Pickle())]
     # Pass OSError exceptions through, presenting the exception message to the user.
-    errors = {OSError: 'OSError'}
+    # errors = {OSError: 'OSError'}
+    errors = {Exception: 'Exception'}
 
 
 
@@ -79,7 +81,7 @@
 
 class Account(Command):
     """
-    This message represents a L{_DirectoryRecord} loaded by the manager process
+    This message represents a L{DirectoryRecord} loaded by the manager process
     being relayed to a worker.
     """
     arguments = [
@@ -106,16 +108,21 @@
 
     @Account.responder
     def account(self, **kw):
-        self.records.append(_DirectoryRecord(**kw))
+        self.records.append(DirectoryRecord(**kw))
         return {}
 
 
     @Configure.responder
-    def config(self, plist):
+    def config(self, cfg):
         from sys import stderr
-        cfg = readPlistFromString(plist)
+        # cfg = readPlistFromString(plist)
+        config = Config.deserializeFromWorker(cfg)
+        with open('logs.txt', 'a') as f:
+            f.write('here')
+            f.write(str(config.__dict__))
+
         addObserver(self.emit)
-        sim = LoadSimulator.fromConfig(cfg)
+        sim = LoadSimulator.fromConfigObject(config)
         sim.records = self.records
         sim.attachServices(stderr)
         return {}
@@ -160,26 +167,29 @@
                             email=record.email,
                             guid=record.guid)
 
-        workerConfig = deepcopy(self.loadsim.configTemplate)
-        # The list of workers is for the manager only; the workers themselves
-        # know they're workers because they _don't_ receive this list.
-        del workerConfig["workers"]
-        # The manager loads the accounts via the configured loader, then sends
-        # them out to the workers (right above), which look at the state at an
-        # instance level and therefore don't need a globally-named directory
-        # record loader.
-        del workerConfig["accounts"]
+        workerConfig = self.loadsim.configTemplate(self.whichWorker, self.numWorkers)
+        dupe = deepcopy(workerConfig)
+        del dupe['records']
+        print dupe
+        # # The list of workers is for the manager only; the workers themselves
+        # # know they're workers because they _don't_ receive this list.
+        # del workerConfig["workers"]
+        # # The manager loads the accounts via the configured loader, then sends
+        # # them out to the workers (right above), which look at the state at an
+        # # instance level and therefore don't need a globally-named directory
+        # # record loader.
+        # del workerConfig["accounts"]
 
-        workerConfig["workerID"] = self.whichWorker
-        workerConfig["workerCount"] = self.numWorkers
-        workerConfig["observers"] = []
-        workerConfig.pop("accounts", None)
+        # workerConfig["workerID"] = self.whichWorker
+        # workerConfig["workerCount"] = self.numWorkers
+        # workerConfig["observers"] = []
+        # workerConfig.pop("accounts", None)
 
-        plist = writePlistToString(workerConfig)
+        # plist = writePlistToString(workerConfig)
         self.output.write("Initiating worker configuration\n")
         def completed(x):
             self.output.write("Worker configuration complete.\n")
-        self.callRemote(Configure, plist=plist).addCallback(completed)
+        self.callRemote(Configure, cfg=workerConfig).addCallback(completed)
 
 
     @LogMessage.responder

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,548 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a OS X client simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the OS_X_10_7 instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-	
-					<!-- OS_X_10_7 can poll the calendar home at some interval. This is
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>30</integer>
-
-					<!-- If the server advertises xmpp push, OS_X_10_7 can wait for notifications
-						about calendar home changes instead of polling for them periodically. If
-						this option is true, then look for the server advertisement for xmpp push
-						and use it if possible. Still fall back to polling if there is no xmpp push
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-
-					<key>supportAmpPush</key>
-					<true/>
-					<key>ampPushHost</key>
-					<string>localhost</string>
-					<key>ampPushPort</key>
-					<integer>62311</integer>
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>60</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.NormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mu gives the mean of the normal distribution (in seconds). -->
-									<key>mu</key>
-									<integer>60</integer>
-
-									<!-- and sigma gives its standard deviation. -->
-									<key>sigma</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>1</integer>
-									<!-- mean - average-->
-									<key>median</key>
-									<integer>6</integer>
-									<!-- maximum -->
-									<key>maximum</key>
-									<real>60</real>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>300</integer>
-									<!-- median - 50% done-->
-									<key>median</key>
-									<integer>1800</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-	</dict>
-</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/clients.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,398 @@
+from caldavclientlibrary.protocol.webdav.definitions import davxml
+
+from contrib.performance.loadtest.ical import BaseAppleClient
+
+from pycalendar.datetime import DateTime
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from twisted.python.filepath import FilePath
+
+def loadRequestBody(clientType, label):
+    return FilePath(__file__).sibling('request-data').child(clientType).child(label + '.request').getContent()
+
+class iOS_5(BaseAppleClient):
+    """
+    Implementation of the iOS 5 network behavior.
+    """
+
+    _client_type = "iOS 5"
+
+    USER_AGENT = "iOS/5.1 (9B179) dataaccessd/1.0"
+
+    # The default interval, used if none is specified in external
+    # configuration.  This is also the actual value used by Snow
+    # Leopard iCal.
+    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
+
+    # The maximum number of resources to retrieve in a single multiget
+    MULTIGET_BATCH_SIZE = 50
+
+    # Override and turn on if client supports Sync REPORT
+    _SYNC_REPORT = False
+
+    # Override and turn off if client does not support attendee lookups
+    _ATTENDEE_LOOKUPS = False
+
+    # Request body data
+    _LOAD_PATH = "iOS_5"
+
+    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
+    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
+    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
+    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
+    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
+
+    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
+    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_CALENDAR_VEVENT_TR_QUERY = loadRequestBody(_LOAD_PATH, 'poll_calendar_vevent_tr_query')
+    _POLL_CALENDAR_VTODO_QUERY = loadRequestBody(_LOAD_PATH, 'poll_calendar_vtodo_query')
+    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
+    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
+    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
+
+    @inlineCallbacks
+    def _pollFirstTime1(self, homeNode, calendars):
+        # Patch calendar properties
+        for cal in calendars:
+            if cal.name != "inbox":
+                yield self._proppatch(
+                    cal.url,
+                    self._STARTUP_PROPPATCH_CALENDAR_COLOR,
+                    method_label="PROPPATCH{calendar}",
+                )
+                yield self._proppatch(
+                    cal.url,
+                    self._STARTUP_PROPPATCH_CALENDAR_ORDER,
+                    method_label="PROPPATCH{calendar}",
+                )
+
+
+    def _pollFirstTime2(self):
+        # Nothing here
+        return succeed(None)
+
+
+    def _updateCalendar(self, calendar, newToken):
+        """
+        Update the local cached data for a calendar in an appropriate manner.
+        """
+        if calendar.name == "inbox":
+            # Inbox is done as a PROPFIND Depth:1
+            return self._updateCalendar_PROPFIND(calendar, newToken)
+        elif "VEVENT" in calendar.componentTypes:
+            # VEVENTs done as time-range VEVENT-only queries
+            return self._updateCalendar_VEVENT(calendar, newToken)
+        elif "VTODO" in calendar.componentTypes:
+            # VTODOs done as VTODO-only queries
+            return self._updateCalendar_VTODO(calendar, newToken)
+
+
+    @inlineCallbacks
+    def _updateCalendar_VEVENT(self, calendar, newToken):
+        """
+        Sync all locally cached VEVENTs using a VEVENT-only time-range query.
+        """
+
+        # Grab old hrefs prior to the PROPFIND so we sync with the old state. We need this because
+        # the sim can fire a PUT between the PROPFIND and when process the removals.
+        old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
+
+        now = DateTime.getNowUTC()
+        now.setDateOnly(True)
+        now.offsetMonth(-1) # 1 month back default
+        result = yield self._report(
+            calendar.url,
+            self._POLL_CALENDAR_VEVENT_TR_QUERY % {"start-date": now.getText()},
+            depth='1',
+            method_label="REPORT{vevent}",
+        )
+
+        yield self._updateApplyChanges(calendar, result, old_hrefs)
+
+        # Now update calendar to the new token
+        self._calendars[calendar.url].changeToken = newToken
+
+
+    @inlineCallbacks
+    def _updateCalendar_VTODO(self, calendar, newToken):
+        """
+        Sync all locally cached VTODOs using a VTODO-only query.
+        """
+
+        # Grab old hrefs prior to the PROPFIND so we sync with the old state. We need this because
+        # the sim can fire a PUT between the PROPFIND and when process the removals.
+        old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
+
+        result = yield self._report(
+            calendar.url,
+            self._POLL_CALENDAR_VTODO_QUERY,
+            depth='1',
+            method_label="REPORT{vtodo}",
+        )
+
+        yield self._updateApplyChanges(calendar, result, old_hrefs)
+
+        # Now update calendar to the new token
+        self._calendars[calendar.url].changeToken = newToken
+
+
+    @inlineCallbacks
+    def startup(self):
+
+        # Try to read data from disk - if it succeeds self.principalURL will be set
+        self.deserialize()
+
+        if self.principalURL is None:
+            # PROPFIND well-known with redirect
+            response = yield self._startupPropfindWellKnown()
+            hrefs = response.getHrefProperties()
+            if davxml.current_user_principal in hrefs:
+                self.principalURL = hrefs[davxml.current_user_principal].toString()
+            elif davxml.principal_URL in hrefs:
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+            else:
+                # PROPFIND principal path to retrieve actual principal-URL
+                response = yield self._principalPropfindInitial(self.record.uid)
+                hrefs = response.getHrefProperties()
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+
+        # Using the actual principal URL, retrieve principal information
+        principal = yield self._extractPrincipalDetails()
+        returnValue(principal)
+
+
+
+class iOS_9(iOS_5): # As far as I can see, the behavior of an iOS 9 client is the same as the behavior of the iOS 5 client.
+    _client_type = "iOS 9"
+
+    USER_AGENT = "iOS/9 dataaccessd/1.0"
+
+    _SYNC_REPORT = False
+
+    _LOAD_PATH = "iOS_5" # Just use the request bodies from iOS 5
+
+
+class OS_X_10_6(BaseAppleClient):
+    """
+    Implementation of the OS X 10.6 iCal network behavior.
+
+    Anything OS X 10.6 iCal does on its own, or any particular
+    network behaviors it takes in response to a user action, belong on
+    this class.
+
+    Usage-profile based behaviors ("the user modifies an event every
+    3.2 minutes") belong elsewhere.
+    """
+
+    _client_type = "OS X 10.6"
+
+    USER_AGENT = "DAVKit/4.0.3 (732); CalendarStore/4.0.3 (991); iCal/4.0.3 (1388); Mac OS X/10.6.4 (10F569)"
+
+    # The default interval, used if none is specified in external
+    # configuration.  This is also the actual value used by Snow
+    # Leopard iCal.
+    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
+
+    # The maximum number of resources to retrieve in a single multiget
+    MULTIGET_BATCH_SIZE = 200
+
+    # Override and turn on if client supports Sync REPORT
+    _SYNC_REPORT = False
+
+    # Override and turn off if client does not support attendee lookups
+    _ATTENDEE_LOOKUPS = True
+
+    # Request body data
+    _LOAD_PATH = "OS_X_10_6"
+
+    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
+    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
+    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
+    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
+    _STARTUP_PRINCIPAL_EXPAND = loadRequestBody(_LOAD_PATH, 'startup_principal_expand')
+    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_TIMEZONE = loadRequestBody(_LOAD_PATH, 'startup_calendar_timezone_proppatch')
+
+    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
+    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
+    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
+    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
+    _POLL_CALENDAR_SYNC_REPORT = None
+    _POLL_NOTIFICATION_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_NOTIFICATION_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_notification_propfind_d1')
+
+    _USER_LIST_PRINCIPAL_PROPERTY_SEARCH = loadRequestBody(_LOAD_PATH, 'user_list_principal_property_search')
+    _POST_AVAILABILITY = loadRequestBody(_LOAD_PATH, 'post_availability')
+
+    @inlineCallbacks
+    def startup(self):
+
+        # Try to read data from disk - if it succeeds self.principalURL will be set
+        self.deserialize()
+
+        if self.principalURL is None:
+            # PROPFIND principal path to retrieve actual principal-URL
+            response = yield self._principalPropfindInitial(self.record.uid)
+            hrefs = response.getHrefProperties()
+            self.principalURL = hrefs[davxml.principal_URL].toString()
+
+        # Using the actual principal URL, retrieve principal information
+        principal = (yield self._extractPrincipalDetails())
+        returnValue(principal)
+
+
+class OS_X_10_7(BaseAppleClient):
+    """
+    Implementation of the OS X 10.7 iCal network behavior.
+    """
+
+    _client_type = "OS X 10.7"
+
+    USER_AGENT = "CalendarStore/5.0.2 (1166); iCal/5.0.2 (1571); Mac OS X/10.7.3 (11D50)"
+
+    # The default interval, used if none is specified in external
+    # configuration.  This is also the actual value used by Snow
+    # Leopard iCal.
+    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
+
+    # The maximum number of resources to retrieve in a single multiget
+    MULTIGET_BATCH_SIZE = 50
+
+    # Override and turn on if client supports Sync REPORT
+    _SYNC_REPORT = True
+
+    # Override and turn off if client does not support attendee lookups
+    _ATTENDEE_LOOKUPS = True
+
+    # Request body data
+    _LOAD_PATH = "OS_X_10_7"
+
+    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
+    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
+    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
+    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
+    _STARTUP_PRINCIPAL_EXPAND = loadRequestBody(_LOAD_PATH, 'startup_principal_expand')
+    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_TIMEZONE = loadRequestBody(_LOAD_PATH, 'startup_calendar_timezone_proppatch')
+
+    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
+    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
+    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
+    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
+    _POLL_CALENDAR_SYNC_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_sync')
+    _POLL_NOTIFICATION_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_NOTIFICATION_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_notification_propfind_d1')
+
+    _USER_LIST_PRINCIPAL_PROPERTY_SEARCH = loadRequestBody(_LOAD_PATH, 'user_list_principal_property_search')
+    _POST_AVAILABILITY = loadRequestBody(_LOAD_PATH, 'post_availability')
+
+
+    @inlineCallbacks
+    def startup(self):
+
+        # Try to read data from disk - if it succeeds self.principalURL will be set
+        self.deserialize()
+
+        if self.principalURL is None:
+            # PROPFIND well-known with redirect
+            response = yield self._startupPropfindWellKnown()
+            hrefs = response.getHrefProperties()
+            if davxml.current_user_principal in hrefs:
+                self.principalURL = hrefs[davxml.current_user_principal].toString()
+            elif davxml.principal_URL in hrefs:
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+            else:
+                # PROPFIND principal path to retrieve actual principal-URL
+                response = yield self._principalPropfindInitial(self.record.uid)
+                hrefs = response.getHrefProperties()
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+
+        # Using the actual principal URL, retrieve principal information
+        principal = yield self._extractPrincipalDetails()
+        returnValue(principal)
+
+
+
+class OS_X_10_11(BaseAppleClient):
+    """
+    Implementation of the OS X 10.11 Calendar.app network behavior.
+    """
+
+    _client_type = "OS X 10.11"
+
+    USER_AGENT = "Mac+OS+X/10.11 (15A216g) CalendarAgent/353"
+
+    # The default interval, used if none is specified in external
+    # configuration.  This is also the actual value used by El
+    # Capital Calendar.app.
+    CALENDAR_HOME_POLL_INTERVAL = 15 * 60  # in seconds
+
+    # The maximum number of resources to retrieve in a single multiget
+    MULTIGET_BATCH_SIZE = 50
+
+    # Override and turn on if client supports Sync REPORT
+    _SYNC_REPORT = True
+
+    # Override and turn off if client does not support attendee lookups
+    _ATTENDEE_LOOKUPS = True
+
+    # Request body data
+    _LOAD_PATH = "OS_X_10_11"
+
+    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known_propfind')
+    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_initial_propfind')
+    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
+    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
+    # _STARTUP_PRINCIPAL_EXPAND = loadRequestBody(_LOAD_PATH, 'startup_principal_expand')
+
+    _STARTUP_CREATE_CALENDAR = loadRequestBody(_LOAD_PATH, 'startup_create_calendar')
+    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
+    # _STARTUP_PROPPATCH_CALENDAR_NAME = loadRequestBody(_LOAD_PATH, 'startup_calendar_displayname_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
+    _STARTUP_PROPPATCH_CALENDAR_TIMEZONE = loadRequestBody(_LOAD_PATH, 'startup_calendar_timezone_proppatch')
+
+    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_depth1_propfind')
+    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_depth1_propfind')
+    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody('OS_X_10_7', 'poll_calendar_multiget')
+    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody('OS_X_10_7', 'poll_calendar_multiget_hrefs')
+    _POLL_CALENDAR_SYNC_REPORT = loadRequestBody('OS_X_10_7', 'poll_calendar_sync')
+    _POLL_NOTIFICATION_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
+    _POLL_NOTIFICATION_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_notification_depth1_propfind')
+
+    _USER_LIST_PRINCIPAL_PROPERTY_SEARCH = loadRequestBody('OS_X_10_7', 'user_list_principal_property_search')
+    _POST_AVAILABILITY = loadRequestBody('OS_X_10_7', 'post_availability')
+
+    _CALENDARSERVER_PRINCIPAL_SEARCH_REPORT = loadRequestBody(_LOAD_PATH, 'principal_search_report')
+
+    @inlineCallbacks
+    def startup(self):
+        # Try to read data from disk - if it succeeds self.principalURL will be set
+        self.deserialize()
+
+        if self.principalURL is None:
+            # print("No cached principal URL found - starting from scratch")
+            # PROPFIND well-known with redirect
+            response = yield self._startupPropfindWellKnown()
+            hrefs = response.getHrefProperties()
+            if davxml.current_user_principal in hrefs:
+                self.principalURL = hrefs[davxml.current_user_principal].toString()
+            elif davxml.principal_URL in hrefs:
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+            else:
+                # PROPFIND principal path to retrieve actual principal-URL
+                response = yield self._principalPropfindInitial(self.record.uid)
+                hrefs = response.getHrefProperties()
+                self.principalURL = hrefs[davxml.principal_URL].toString()
+        # print("Principal URL: " + self.principalURL)
+
+        # Using the actual principal URL, retrieve principal information
+        principal = yield self._extractPrincipalDetails()
+        # print("Principal: " + str(principal))
+        returnValue(principal)

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.dist.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.dist.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.dist.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,188 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- This is a distributed orchestrator configuration; 'workers' is a list of
-							shell commands to run sub-processes.
-							-->
-		<key>workers</key>
-		<array>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
-		</array>
-
-		<!-- Identify the server to be load tested. -->
-		<key>server</key>
-		<string>https://127.0.0.1:8443</string>
-
-		<!-- The template URI for doing initial principal lookup on. -->
-		<key>principalPathTemplate</key>
-		<string>/principals/users/%s/</string>
-
-		<!-- Configure Admin Web UI. -->
-		<key>webadmin</key>
-		<dict>
-			<key>enabled</key>
-			<true/>
-			
-			<key>HTTPPort</key>
-			<integer>8080</integer>
-		</dict>
-
-		<!--  Define whether server supports stats socket. -->
-		<key>serverStats</key>
-		<dict>
-			<key>enabled</key>
-			<true/>
-			<key>Port</key>
-			<integer>8100</integer>
-		</dict>
-
-		<!--  Define whether client data should be re-used. It will always be saved to the specified path.-->
-		<key>clientDataSerialization</key>
-		<dict>
-			<key>UseOldData</key>
-			<true/>
-			<key>Path</key>
-			<string>/tmp/sim</string>
-		</dict>
-
-		<!-- Define the credentials of the clients which will be used to load test 
-			the server. These credentials must already be valid on the server. -->
-		<key>accounts</key>
-		<dict>
-			<!-- The loader is the fully-qualified Python name of a callable which 
-				returns a list of directory service records defining all of the client accounts 
-				to use. contrib.performance.loadtest.sim.recordsFromCSVFile reads username, 
-				password, mailto triples from a CSV file and returns them as a list of faked 
-				directory service records. -->
-			<key>loader</key>
-			<string>contrib.performance.loadtest.sim.recordsFromCSVFile</string>
-
-			<!-- Keyword arguments may be passed to the loader. -->
-			<key>params</key>
-			<dict>
-				<!-- recordsFromCSVFile interprets the path relative to the config.plist, 
-					to make it independent of the script's working directory while still allowing 
-					a relative path. This isn't a great solution. -->
-				<key>path</key>
-				<string>contrib/performance/loadtest/accounts.csv</string>
-			</dict>
-		</dict>
-
-		<!-- Define how many clients will participate in the load test and how 
-			they will show up. -->
-		<key>arrival</key>
-		<dict>
-
-			<!-- Specify a class which creates new clients and introduces them into 
-				the test. contrib.performance.loadtest.population.SmoothRampUp introduces 
-				groups of new clients at fixed intervals up to a maximum. The size of the 
-				group, interval, and maximum are configured by the parameters below. The 
-				total number of clients is groups * groupSize, which needs to be no larger 
-				than the number of credentials created in the accounts section. -->
-			<key>factory</key>
-			<string>contrib.performance.loadtest.population.SmoothRampUp</string>
-
-			<key>params</key>
-			<dict>
-				<!-- groups gives the total number of groups of clients to introduce. -->
-				<key>groups</key>
-				<integer>99</integer>
-
-				<!-- groupSize is the number of clients in each group of clients. It's 
-					really only a "smooth" ramp up if this is pretty small. -->
-				<key>groupSize</key>
-				<integer>1</integer>
-
-				<!-- Number of seconds between the introduction of each group. -->
-				<key>interval</key>
-				<integer>3</integer>
-
-				<!-- Number of clients each user is assigned to. -->
-				<!-- Set weight of clients to 1 if this is > 1. Number of clients must match this value if > 1. -->
-				<key>clientsPerUser</key>
-				<integer>1</integer>
-			</dict>
-
-		</dict>
-
-		<!-- Define some log observers to report on the load test. -->
-		<key>observers</key>
-		<array>
-			<!-- ReportStatistics generates an end-of-run summary of the HTTP requests 
-				made, their timings, and their results. -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.population.ReportStatistics</string>
-				<key>params</key>
-				<dict>
-					<!-- The thresholds for each request type -->
-					<key>thresholdsPath</key>
-					<string>contrib/performance/loadtest/thresholds.json</string>
-					
-					<!-- The benchmarks for overall QoS -->
-					<key>benchmarksPath</key>
-					<string>contrib/performance/loadtest/benchmarks.json</string>
-
-					<!-- The % of failures that constitute a failed test -->
-					<key>failCutoff</key>
-					<real>1.0</real>
-				</dict>
-			</dict>
-	
-			<!-- RequestLogger generates a realtime log of all HTTP requests made 
-				during the load test. -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.ical.RequestLogger</string>
-				<key>params</key>
-				<dict>
-				</dict>
-			</dict>
-	
-			<!-- OperationLogger generates an end-of-run summary of the gross operations 
-				performed (logical operations which may span more than one HTTP request, 
-				such as inviting an attendee to an event). -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.profiles.OperationLogger</string>
-				<key>params</key>
-				<dict>
-					<!-- The thresholds for each operation type -->
-					<key>thresholdsPath</key>
-					<string>contrib/performance/loadtest/thresholds.json</string>
-					
-					<!-- The % of operations beyond the lag cut-off that constitute a failed test -->
-					<key>lagCutoff</key>
-					<real>1.0</real>
-					
-					<!-- The % of failures that constitute a failed test -->
-					<key>failCutoff</key>
-					<real>1.0</real>
-				</dict>
-			</dict>
-		</array>
-	</dict>
-</plist>

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,175 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Identify the server to be load tested. -->
-		<key>server</key>
-		<string>https://127.0.0.1:8443</string>
-
-		<!-- The template URI for doing initial principal lookup on. -->
-		<key>principalPathTemplate</key>
-		<string>/principals/users/%s/</string>
-
-		<!-- Configure Admin Web UI. -->
-		<key>webadmin</key>
-		<dict>
-			<key>enabled</key>
-			<true/>
-
-			<key>HTTPPort</key>
-			<integer>8080</integer>
-		</dict>
-
-		<!--  Define whether server supports stats socket. -->
-		<key>serverStats</key>
-		<dict>
-			<key>enabled</key>
-			<true/>
-			<key>Port</key>
-			<integer>8100</integer>
-		</dict>
-
-		<!--  Define whether client data should be re-used. It will always be saved to the specified path.-->
-		<key>clientDataSerialization</key>
-		<dict>
-			<key>UseOldData</key>
-			<true/>
-			<key>Path</key>
-			<string>/tmp/sim</string>
-		</dict>
-
-		<!-- Define the credentials of the clients which will be used to load test
-			the server. These credentials must already be valid on the server. -->
-		<key>accounts</key>
-		<dict>
-			<!-- The loader is the fully-qualified Python name of a callable which
-				returns a list of directory service records defining all of the client accounts
-				to use. contrib.performance.loadtest.sim.recordsFromCSVFile reads username,
-				password, mailto triples from a CSV file and returns them as a list of faked
-				directory service records. -->
-			<key>loader</key>
-			<string>contrib.performance.loadtest.sim.recordsFromCSVFile</string>
-
-			<!-- Keyword arguments may be passed to the loader. -->
-			<key>params</key>
-			<dict>
-				<!-- recordsFromCSVFile interprets the path relative to the config.plist,
-					to make it independent of the script's working directory while still allowing
-					a relative path. This isn't a great solution. -->
-				<key>path</key>
-				<string>contrib/performance/loadtest/accounts.csv</string>
-			</dict>
-		</dict>
-
-		<!-- Define how many clients will participate in the load test and how
-			they will show up. -->
-		<key>arrival</key>
-		<dict>
-
-			<!-- Specify a class which creates new clients and introduces them into
-				the test. contrib.performance.loadtest.population.SmoothRampUp introduces
-				groups of new clients at fixed intervals up to a maximum. The size of the
-				group, interval, and maximum are configured by the parameters below. The
-				total number of clients is groups * groupSize, which needs to be no larger
-				than the number of credentials created in the accounts section. -->
-			<key>factory</key>
-			<string>contrib.performance.loadtest.population.SmoothRampUp</string>
-
-			<key>params</key>
-			<dict>
-				<!-- groups gives the total number of groups of clients to introduce. -->
-				<key>groups</key>
-				<integer>20</integer>
-
-				<!-- groupSize is the number of clients in each group of clients. It's
-					really only a "smooth" ramp up if this is pretty small. -->
-				<key>groupSize</key>
-				<integer>1</integer>
-
-				<!-- Number of seconds between the introduction of each group. -->
-				<key>interval</key>
-				<integer>3</integer>
-
-				<!-- Number of clients each user is assigned to. -->
-				<!-- Set weight of clients to 1 if this is > 1. Number of clients must match this value if > 1. -->
-				<key>clientsPerUser</key>
-				<integer>1</integer>
-			</dict>
-
-		</dict>
-
-		<!-- Define some log observers to report on the load test. -->
-		<key>observers</key>
-		<array>
-			<!-- ReportStatistics generates an end-of-run summary of the HTTP requests 
-				made, their timings, and their results. -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.population.ReportStatistics</string>
-				<key>params</key>
-				<dict>
-					<!-- The thresholds for each request type -->
-					<key>thresholdsPath</key>
-					<string>contrib/performance/loadtest/thresholds.json</string>
-					
-					<!-- The benchmarks for overall QoS -->
-					<key>benchmarksPath</key>
-					<string>contrib/performance/loadtest/benchmarks.json</string>
-
-					<!-- The % of failures that constitute a failed test -->
-					<key>failCutoff</key>
-					<real>1.0</real>
-				</dict>
-			</dict>
-	
-			<!-- RequestLogger generates a realtime log of all HTTP requests made 
-				during the load test. -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.ical.RequestLogger</string>
-				<key>params</key>
-				<dict>
-				</dict>
-			</dict>
-	
-			<!-- OperationLogger generates an end-of-run summary of the gross operations 
-				performed (logical operations which may span more than one HTTP request, 
-				such as inviting an attendee to an event). -->
-			<dict>
-				<key>type</key>
-				<string>contrib.performance.loadtest.profiles.OperationLogger</string>
-				<key>params</key>
-				<dict>
-					<!-- The thresholds for each operation type -->
-					<key>thresholdsPath</key>
-					<string>contrib/performance/loadtest/thresholds.json</string>
-					
-					<!-- The % of operations beyond the lag cut-off that constitute a failed test -->
-					<key>lagCutoff</key>
-					<real>1.0</real>
-					
-					<!-- The % of failures that constitute a failed test -->
-					<key>failCutoff</key>
-					<real>1.0</real>
-				</dict>
-			</dict>
-		</array>
-	</dict>
-</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/config.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,186 @@
+from importlib import import_module
+
+from twisted.python.log import msg
+from contrib.performance.loadtest.logger import ReportStatistics, RequestLogger, OperationLogger
+from contrib.performance.loadtest.records import recordsFromCSVFile
+from contrib.performance.loadtest.population import ClientFactory, PopulationParameters
+
+class Config(object):
+    def __init__(self):
+        pass
+
+    def populateFrom(self, serverConfig, clientConfig, usePlist=False):
+        # If there is a list of workers, then this process is *not* a worker
+        isManager = serverConfig.get('workers') is not None
+
+        if usePlist:
+            # If the supplied files are plists, we need to convert the named objects into real Python objects.
+            # The ensuing hacky code is why I recommend we remove support for plist-based configuration
+            workers = config['workers']
+            if not isManager:
+                # Client / place where the simulator actually runs configuration
+                workerID = config.get("workerID", 0)
+                workerCount = config.get("workerCount", 1)
+                configTemplate = None
+                server = config.get('server', 'http://127.0.0.1:8008')
+                serializationPath = None
+
+                serializationPath = config['serializationPath']
+
+                if 'arrival' in config:
+                    arrival = Arrival(
+                        namedAny(config['arrival']['factory']),
+                        config['arrival']['params'])
+                else:
+                    arrival = Arrival(
+                        SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
+
+                parameters = PopulationParameters()
+                if 'clients' in config:
+                    for clientConfig in config['clients']:
+                        parameters.addClient(
+                            clientConfig["weight"],
+                            ClientType(
+                                clientConfig["software"],
+                                clientConfig["params"],
+                                clientConfig["profiles"]
+                            )
+                        )
+                            # ClientType(
+                            #     namedAny(clientConfig["software"]),
+                            #     cls._convertParams(clientConfig["params"]),
+                            #     [
+                            #         ProfileType(
+                            #             namedAny(profile["class"]),
+                            #             cls._convertParams(profile["params"])
+                            #         ) for profile in clientConfig["profiles"]
+                            #     ]))
+                if not parameters.clients:
+                    parameters.addClient(1,
+                                         ClientType(OS_X_10_6, {},
+                                                    [Eventer, Inviter, Accepter]))
+            else:
+                # Manager / observer process.
+                server = ''
+                serializationPath = None
+                arrival = None
+                parameters = None
+                workerID = 0
+                configTemplate = config
+                workerCount = 1
+
+            # webadminPort = 
+            webadminPort = None
+            if 'webadmin' in config:
+                if config['webadmin']['enabled']:
+                    webadminPort = config['webadmin']['HTTPPort']
+
+            serverStats = None
+            if 'serverStats' in config:
+                if config['serverStats']['enabled']:
+                    serverStats = config['serverStats']
+                    serverStats['server'] = config['server'] if 'server' in config else ''
+
+            observers = []
+            if 'observers' in config:
+                for observer in config['observers']:
+                    observerName = observer["type"]
+                    observerParams = observer["params"]
+                    observers.append(namedAny(observerName)(**observerParams))
+
+            records = []
+            if 'accounts' in config:
+                loader = config['accounts']['loader']
+                params = config['accounts']['params']
+                records.extend(namedAny(loader)(**params))
+                output.write("Loaded {0} accounts.\n".format(len(records)))
+
+        else:
+            # Python configuration - super easy! Look! It's great!
+                self.webadminPort = serverConfig.get('webadminPort')
+                self.serverStats = serverConfig.get('serverStatsPort')
+                self.observers = serverConfig.get('observers') # Workers shouldn't need this
+                self.workers = serverConfig.get('workers')
+
+                self.server = serverConfig.get('server')
+                self.serializationPath = serverConfig.get('serializationPath')
+                self.arrival = serverConfig.get('arrival')
+                self.records = serverConfig.get('records')
+                self.workerID = serverConfig.get('workerID', 0)
+                self.workerCount = serverConfig.get('workerCount', 1)
+                self.parameters = self.buildParameters(clientConfig)
+
+    def buildParameters(self, clients):
+        parameters = PopulationParameters()
+        for client in clients:
+            parameters.addClient(
+                client["weight"],
+                ClientFactory(
+                    client["software"],
+                    client["params"],
+                    client["profiles"]
+                )
+            )
+        return parameters
+
+    def buildSerializationPath(self):
+        if self.serializationPath:
+            if not isdir(serializationPath):
+                try:
+                    mkdir(serializationPath)
+                except OSError:
+                    print("Unable to create client data serialization directory: %s" % (serializationPath))
+                    print("Please consult the clientDataSerialization stanza of contrib/performance/loadtest/config.plist")
+                    raise
+
+    def serializeForWorker(self, workerID, workerCount):
+        if not self.workers: # If we are workers, don't try to be a manager
+            return {}
+        # print "Trying to serialize for worker #" + str(workerID)
+        # print "My info, btw is " + str(self.__dict__)
+        info = {
+            'webadminPort': '',
+            'serverStats': '',
+            'workers': [],
+            'observers': [],
+            'workerID': workerID,
+            'workerCount': workerCount,
+            # Workers need some information to work correctly
+            'server': self.server,
+            'serializationPath': self.serializationPath,
+            'arrival': self.arrival,
+            'records': self.records,
+            'parameters': self.parameters
+        }
+        return info
+
+    @classmethod
+    def deserializeFromWorker(cls, info):
+        base = cls()
+        base.__dict__.update(info)
+        return base
+
+    # Goodness, how awkward is this code? If we dropped support for plists, we could do away with it
+    @classmethod
+    def _convertParams(cls, params):
+        """
+        Find parameter values which should be more structured than plistlib is
+        capable of constructing and replace them with the more structured form.
+
+        Specifically, find keys that end with C{"Distribution"} and convert
+        them into some kind of distribution object using the associated
+        dictionary of keyword arguments.
+        """
+        for k, v in params.iteritems():
+            if k.endswith('Distribution'): # Goodness how fragile
+                params[k] = cls._convertDistribution(v)
+        return params
+
+
+    @classmethod
+    def _convertDistribution(cls, value):
+        """
+        Construct and return a new distribution object using the type and
+        params specified by C{value}.
+        """
+        return namedAny(value['type'])(**value['params'])
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/debug.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/debug.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/debug.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,27 @@
+def printargs(func):
+    """
+    This decorator prints the arguments passed to a function before calling it
+
+    Example:
+        @printargs
+        def foo(a, b, c, *args, **kwargs):
+            pass
+
+        foo(1, 2, 3, 4, 5, x=6, y=7)
+        # prints `foo(a:1, b:2, c:3, args=(4, 5), kwargs={'y': 7, 'x': 6})`
+
+    """
+    fname = func.func_name
+    fc = func.func_code
+    argcount = fc.co_argcount
+    argnames = fc.co_varnames[:argcount]
+    def wrapper(*args, **kwargs):
+        named_args = ', '.join(['{0}: {1}'.format(arg, val) for arg, val in zip(argnames, args[:argcount])])
+        print "{0}({1}, args={2}, kwargs={3})".format(
+            fname,
+            named_args,
+            args[argcount:],
+            kwargs
+        )
+        return func(*args, **kwargs)
+    return wrapper

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/distributions.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/distributions.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/distributions.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,440 @@
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Implementation of a statistics library for Calendar performance analysis.
+Exports:
+
+IDistribution interface exposes:
+  sample()
+
+Sampling from this distribution must *not* change the underlying behavior of a distribution
+
+Distributions (all of which implement IDistribution):
+  # Discrete Distributions / Finite Support
+  Bernoulli
+  Binomial
+  Rademacher
+  Fixed
+  UniformDiscrete
+  UniformInteger
+
+  # Discrete Distributions / Infinite Support (> 0)
+  Poisson
+  Geometric
+
+  LogNormal
+
+  Normal
+  UniformReal
+  Triangular
+  Beta
+  ChiSquared
+  Exponential
+  Gamma
+
+  # CalendarServer Specific
+  NearFuture
+  Work
+  Recurrence
+
+# TODO
+Implement simple ones through large ones
+Squeeze / pinch
+"""
+from math import log, sqrt
+from time import mktime
+import random
+import numpy.random as nprandom
+
+from pycalendar.datetime import DateTime
+from pycalendar.duration import Duration as PyDuration
+from pycalendar.icalendar.property import Property
+from pycalendar.timezone import Timezone
+
+from zope.interface import Interface, implements
+from twisted.python.util import FancyEqMixin
+
+
+class IDistribution(Interface):
+    """Interface for a class that provides a single function, `sample`, which returns a float"""
+    def sample(): #@NoSelf
+        pass
+
+
+class UniformDiscreteDistribution(object, FancyEqMixin):
+    """
+
+    """
+    implements(IDistribution)
+
+    compareAttributes = ['_values']
+
+    def __init__(self, values):
+        self._values = values
+
+    def sample(self):
+        return random.choice(self._values)
+
+
+
+class LogNormalDistribution(object, FancyEqMixin):
+    """
+    """
+    implements(IDistribution)
+
+    compareAttributes = ['_mu', '_sigma', '_maximum']
+
+    def __init__(self, mu=None, sigma=None, mean=None, mode=None, median=None, maximum=None):
+
+        if mu is not None and sigma is not None:
+            scale = 1.0
+        elif not (mu is None and sigma is None):
+            raise ValueError("mu and sigma must both be defined or both not defined")
+        elif mode is None:
+            raise ValueError("When mu and sigma are not defined, mode must be defined")
+        elif median is not None:
+            scale = mode
+            median /= mode
+            mode = 1.0
+            mu = log(median)
+            sigma = sqrt(log(median) - log(mode))
+        elif mean is not None:
+            scale = mode
+            mean /= mode
+            mode = 1.0
+            mu = log(mean) + log(mode) / 2.0
+            sigma = sqrt(log(mean) - log(mode) / 2.0)
+        else:
+            raise ValueError("When using mode one of median or mean must be defined")
+
+        self._mu = mu
+        self._sigma = sigma
+        self._scale = scale
+        self._maximum = maximum
+
+
+    def sample(self):
+        result = self._scale * random.lognormvariate(self._mu, self._sigma)
+        if self._maximum is not None and result > self._maximum:
+            for _ignore_i in range(10):
+                result = self._scale * random.lognormvariate(self._mu, self._sigma)
+                if result <= self._maximum:
+                    break
+            else:
+                raise ValueError("Unable to generate LogNormalDistribution sample within required range")
+        return result
+
+
+
+class FixedDistribution(object, FancyEqMixin):
+    """
+    """
+    implements(IDistribution)
+
+    compareAttributes = ['_value']
+
+    def __init__(self, value):
+        self._value = value
+
+
+    def sample(self):
+        return self._value
+
+
+
+class NearFutureDistribution(object, FancyEqMixin):
+    compareAttributes = ['_offset']
+
+    def __init__(self):
+        self._offset = LogNormalDistribution(7, 0.8)
+
+
+    def sample(self):
+        now = DateTime.getNowUTC()
+        now.offsetSeconds(int(self._offset.sample()))
+        return now
+
+
+
+class NormalDistribution(object, FancyEqMixin):
+    compareAttributes = ['_mu', '_sigma']
+
+    def __init__(self, mu, sigma):
+        self._mu = mu
+        self._sigma = sigma
+
+
+    def sample(self):
+        # Only return positive values or zero
+        v = random.normalvariate(self._mu, self._sigma)
+        while v < 0:
+            v = random.normalvariate(self._mu, self._sigma)
+        return v
+
+
+
+class UniformIntegerDistribution(object, FancyEqMixin):
+    compareAttributes = ['_min', '_max']
+
+    def __init__(self, min, max):
+        self._min = min
+        self._max = max
+
+
+    def sample(self):
+        return int(random.uniform(self._min, self._max))
+
+
+class UniformRealDistribution(object, FancyEqMixin):
+    compareAttributes = ['_min', '_max']
+
+    def __init__(self, min, max):
+        self._min = min
+        self._max = max
+
+
+    def sample(self):
+        return random.uniform(self._min, self._max)
+
+
+class BernoulliDistribution(object, FancyEqMixin):
+    compareAttributes = ["_p"]
+
+    def __init__(self, proportion=0.5):
+        """Initializes a bernoulli distribution with success probability given by p
+        Prereq: 0 <= p <= 1
+        Returns 1 with probability p, 0 with probability q = 1-p
+        """
+        self._p = proportion
+
+    def sample(self):
+        return 1 if random.random() <= self._p else 0
+
+
+class RademacherDistribution(object, FancyEqMixin):
+    """
+    Takes value 1 with probability 1/2 and value -1 with probability 1/2
+    """
+    def __init__(self):
+        """
+        """
+        self._d = BernoulliDistribution(proportion=0.5)
+
+    def sample(self):
+        return [-1, 1][self._d.sample()]
+
+
+
+class BinomialDistribution(object, FancyEqMixin):
+    compareAttributes = ["_successProbability", "_numTrials"]
+
+    def __init__(self, p=0.5, n=10):
+        self._successProbability = p
+        self._numTrials = n
+
+    def sample(self):
+        return nprandom.binomial(self._numTrials, self._successProbability)
+
+
+
+class TriangularDistribution(object, FancyEqMixin):
+    compareAttributes = ["_left", "_mode", "_right"]
+
+    def __init__(self, left, mode, right):
+        self._left = left
+        self._mode = mode
+        self._right = right
+
+    def sample(self):
+        return nprandom.triangular(self._left, self._mode, self._right)
+
+
+class GeometricDistribution(object, FancyEqMixin):
+    """
+    Expected number of Bernoulli trials before the first success
+    """
+    compareAttributes = ["_p"]
+    def __init__(self, proportion=0.5):
+        self._p = proportion
+
+    def sample(self):
+        return nprandom.geometric(self._p)
+
+
+class PoissonDistribution(object, FancyEqMixin):
+    compareAttributes = ["_lambda"]
+    def __init__(self, lam):
+        self._lambda = lam
+
+    def sample(self):
+        return nprandom.possion(self._lambda)
+
+
+class BetaDistribution(object, FancyEqMixin):
+    compareAttributes = ["_alpha", "_beta"]
+    def __init__(self, alpha, beta):
+        self._alpha = alpha
+        self._beta = beta
+
+    def sample(self):
+        return nprandom.beta(self._alpha, self._beta)
+
+
+class ChiSquaredDistribution(object, FancyEqMixin):
+    compareAttributes = ["_df"]
+    def __init__(self, degreesOfFreedom):
+        self._df = degreesOfFreedom
+
+    def sample(self):
+        return nprandom.chisquare(self._df)
+
+
+class ExponentialDistribution(object, FancyEqMixin):
+    compareAttributes = ["_scale"]
+    def __init__(self, scale):
+        self._scale = scale
+
+    def sample(self):
+        return nprandom.exponential(self._scale)
+
+
+
+class GammaDistribution(object, FancyEqMixin):
+    compareAttributes = ["_shape", "_scale"]
+    def __init__(self, shape, scale=1.0):
+        self._shape = shape
+        self._scale = scale
+
+    def sample(self):
+        return nprandom.gamma(self._shape, self._scale)
+
+NUM_WEEKDAYS = 7
+
+class WorkDistribution(object, FancyEqMixin):
+    compareAttributes = ["_daysOfWeek", "_beginHour", "_endHour"]
+
+    _weekdayNames = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]
+
+    def __init__(self, daysOfWeek=["mon", "tue", "wed", "thu", "fri"], beginHour=8, endHour=17, tzname="UTC"):
+        self._daysOfWeek = [self._weekdayNames.index(day) for day in daysOfWeek]
+        self._beginHour = beginHour
+        self._endHour = endHour
+        self._tzname = tzname
+        self._helperDistribution = NormalDistribution(
+            # Mean 6 workdays in the future
+            60 * 60 * 8 * 6,
+            # Standard deviation of 4 workdays
+            60 * 60 * 8 * 4)
+        self.now = DateTime.getNow
+
+
+    def astimestamp(self, dt):
+        return mktime(dt.timetuple())
+
+
+    def _findWorkAfter(self, when):
+        """
+        Return a two-tuple of the start and end of work hours following
+        C{when}.  If C{when} falls within work hours, then the start time will
+        be equal to when.
+        """
+        # Find a workday that follows the timestamp
+        weekday = when.getDayOfWeek()
+        for i in range(NUM_WEEKDAYS):
+            day = when + PyDuration(days=i)
+            if (weekday + i) % NUM_WEEKDAYS in self._daysOfWeek:
+                # Joy, a day on which work might occur.  Find the first hour on
+                # this day when work may start.
+                day.setHHMMSS(self._beginHour, 0, 0)
+                begin = day
+                end = begin.duplicate()
+                end.setHHMMSS(self._endHour, 0, 0)
+                if end > when:
+                    return begin, end
+
+
+    def sample(self):
+        offset = PyDuration(seconds=int(self._helperDistribution.sample()))
+        beginning = self.now(Timezone(tzid=self._tzname))
+        while offset:
+            start, end = self._findWorkAfter(beginning)
+            if end - start > offset:
+                result = start + offset
+                result.setMinutes(result.getMinutes() // 15 * 15)
+                result.setSeconds(0)
+                return result
+            offset.setDuration(offset.getTotalSeconds() - (end - start).getTotalSeconds())
+            beginning = end
+
+
+
+class RecurrenceDistribution(object, FancyEqMixin):
+    compareAttributes = ["_allowRecurrence", "_weights"]
+
+    _model_rrules = {
+        "none": None,
+        "daily": "RRULE:FREQ=DAILY",
+        "weekly": "RRULE:FREQ=WEEKLY",
+        "monthly": "RRULE:FREQ=MONTHLY",
+        "yearly": "RRULE:FREQ=YEARLY",
+        "dailylimit": "RRULE:FREQ=DAILY;COUNT=14",
+        "weeklylimit": "RRULE:FREQ=WEEKLY;COUNT=4",
+        "workdays": "RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
+    }
+
+    def __init__(self, allowRecurrence, weights={}):
+        self._allowRecurrence = allowRecurrence
+        self._rrules = []
+        if self._allowRecurrence:
+            for rrule, count in sorted(weights.items(), key=lambda x: x[0]):
+                for _ignore in range(count):
+                    self._rrules.append(self._model_rrules[rrule])
+        self._helperDistribution = UniformIntegerDistribution(0, len(self._rrules) - 1)
+
+
+    def sample(self):
+
+        if self._allowRecurrence:
+            index = self._helperDistribution.sample()
+            rrule = self._rrules[index]
+            if rrule:
+                prop = Property.parseText(rrule)
+                return prop
+
+        return None
+
+if __name__ == '__main__':
+    from collections import defaultdict
+    mu = 15
+    sigma = 12
+    print("Testing LogNormalDistribution with mu={mu}, sigma={sigma}".format(
+        mu=mu, sigma=sigma
+    ))
+    distribution = LogNormalDistribution(mu, sigma, 100)
+    result = defaultdict(int)
+    for _ignore_i in xrange(100000):
+        s = int(distribution.sample())
+        if s > 300:
+            continue
+        result[s] += 1
+
+    total = 0
+    for k, v in sorted(result.items(), key=lambda x: x[0]):
+        print("%d\t%.5f" % (k, float(v) / result[1]))
+        total += k * v
+
+    print("Average: %.2f" % (float(total) / sum(result.values()),))

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ical.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ical.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/ical.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -18,40 +18,28 @@
 
 from caldavclientlibrary.protocol.caldav.definitions import caldavxml
 from caldavclientlibrary.protocol.caldav.definitions import csxml
-from caldavclientlibrary.protocol.url import URL
 from caldavclientlibrary.protocol.webdav.definitions import davxml
-from caldavclientlibrary.protocol.webdav.propfindparser import PropFindParser
+from caldavclientlibrary.protocol.url import URL
 
-from calendarserver.push.amppush import subscribeToIDs
-from calendarserver.tools.notifications import PubSubClientFactory
+from contrib.performance.httpclient import readBody
+from contrib.performance.loadtest.pubsub import Publisher
+from contrib.performance.loadtest.resources import Event, Calendar
+from contrib.performance.loadtest.requester import Requester, IncorrectResponseCode
+from contrib.performance.loadtest.push import PushMonitor
 
-from contrib.performance.httpauth import AuthHandlerAgent
-from contrib.performance.httpclient import StringProducer, readBody
-from contrib.performance.loadtest.subscribe import Periodical
-
 from pycalendar.datetime import DateTime
 from pycalendar.duration import Duration
 from pycalendar.timezone import Timezone
 
-from twext.internet.adaptendpoint import connect
-from twext.internet.gaiendpoint import GAIEndpoint
-from twisted.internet.ssl import ClientContextFactory
-
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue, \
-    succeed
 from twisted.internet.task import LoopingCall
-from twisted.python.filepath import FilePath
-from twisted.python.log import addObserver, err, msg
-from twisted.python.util import FancyEqMixin
-from twisted.web.client import Agent, ContentDecoderAgent, GzipDecoder, \
-    _DeprecatedToCurrentPolicyForHTTPS
-from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT, PRECONDITION_FAILED, MOVED_PERMANENTLY, \
-    FORBIDDEN, FOUND
+from twisted.internet.defer import succeed, Deferred, inlineCallbacks, returnValue
+from twisted.python.log import err, msg
+from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT, FORBIDDEN, PRECONDITION_FAILED, MOVED_PERMANENTLY, FOUND
 from twisted.web.http_headers import Headers
 
 from twistedcaldav.ical import Component, Property
 
-from urlparse import urlparse, urlunparse, urlsplit, urljoin
+from urlparse import urlparse, urlsplit
 from uuid import uuid4
 from xml.etree import ElementTree
 
@@ -59,196 +47,32 @@
 import os
 import random
 
-ElementTree.QName.__repr__ = lambda self: '<QName %r>' % (self.text,)
+"""
+run
+  startup
+    deserialize
+    _startupPropfindWellKnown
+    _principalPropfindInitial
+    _extractPrincipalDetails
+    _checkCalendarsForEvents
+"""
 
-def loadRequestBody(clientType, label):
-    return FilePath(__file__).sibling('request-data').child(clientType).child(label + '.request').getContent()
 
+ElementTree.QName.__repr__ = lambda self: '<QName %r>' % (self.text,)
 
-SUPPORTED_REPORT_SET = '{DAV:}supported-report-set'
+SUPPORTED_REPORT_SET = davxml.supported_report_set.text
 
-class IncorrectResponseCode(Exception):
-    """
-    Raised when a response has a code other than the one expected.
+class Attendee(Property):
+    def __init__(self, ):
+        pass
 
-    @ivar expected: The response codes which was expected.
-    @type expected: C{tuple} of C{int}
 
-    @ivar response: The response which was received
-    @type response: L{twisted.web.client.Response}
-    """
-    def __init__(self, expected, response):
-        self.expected = expected
-        self.response = response
-
-
-
 class MissingCalendarHome(Exception):
     """
     Raised when the calendar home for a user is 404
     """
 
 
-
-class XMPPPush(object, FancyEqMixin):
-    """
-    This represents an XMPP PubSub location where push notifications for
-    particular calendar home might be received.
-    """
-    compareAttributes = ('server', 'uri', 'pushkey')
-
-    def __init__(self, server, uri, pushkey):
-        self.server = server
-        self.uri = uri
-        self.pushkey = pushkey
-
-
-
-def u2str(data):
-    return data.encode("utf-8") if type(data) is unicode else data
-
-
-
-class Event(object):
-    def __init__(self, serializeBasePath, url, etag, component=None):
-        self.serializeBasePath = serializeBasePath
-        self.url = url
-        self.etag = etag
-        self.scheduleTag = None
-        if component is not None:
-            self.component = component
-        self.uid = component.resourceUID() if component is not None else None
-
-
-    def getUID(self):
-        """
-        Return the UID of the calendar resource.
-        """
-        return self.uid
-
-
-    def serializePath(self):
-        if self.serializeBasePath:
-            calendar = os.path.join(self.serializeBasePath, self.url.split("/")[-2])
-            if not os.path.exists(calendar):
-                os.makedirs(calendar)
-            return os.path.join(calendar, self.url.split("/")[-1])
-        else:
-            return None
-
-
-    def serialize(self):
-        """
-        Create a dict of the data so we can serialize as JSON.
-        """
-
-        result = {}
-        for attr in ("url", "etag", "scheduleTag", "uid",):
-            result[attr] = getattr(self, attr)
-        return result
-
-
-    @staticmethod
-    def deserialize(serializeLocation, data):
-        """
-        Convert dict (deserialized from JSON) into an L{Event}.
-        """
-
-        event = Event(serializeLocation, None, None)
-        for attr in ("url", "etag", "scheduleTag", "uid",):
-            setattr(event, attr, u2str(data[attr]))
-        return event
-
-
-    @property
-    def component(self):
-        """
-        Data always read from disk - never cached in the object.
-        """
-        path = self.serializePath()
-        if path and os.path.exists(path):
-            f = open(path)
-            comp = Component.fromString(f.read())
-            f.close()
-            return comp
-        else:
-            return None
-
-
-    @component.setter
-    def component(self, component):
-        """
-        Data always written to disk - never cached on the object.
-        """
-        path = self.serializePath()
-        if path:
-            if component is None:
-                os.remove(path)
-            else:
-                f = open(path, "w")
-                f.write(str(component))
-                f.close()
-        self.uid = component.resourceUID() if component is not None else None
-
-
-    def removed(self):
-        """
-        Resource no longer exists on the server - remove associated data.
-        """
-        path = self.serializePath()
-        if path and os.path.exists(path):
-            os.remove(path)
-
-
-
-class Calendar(object):
-    def __init__(self, resourceType, componentTypes, name, url, changeToken):
-        self.resourceType = resourceType
-        self.componentTypes = componentTypes
-        self.name = name
-        self.url = url
-        self.changeToken = changeToken
-        self.events = {}
-
-        if self.name is None and self.url is not None:
-            self.name = self.url.rstrip("/").split("/")[-1]
-
-
-    def serialize(self):
-        """
-        Create a dict of the data so we can serialize as JSON.
-        """
-
-        result = {}
-        for attr in ("resourceType", "name", "url", "changeToken"):
-            result[attr] = getattr(self, attr)
-        result["componentTypes"] = list(sorted(self.componentTypes))
-        result["events"] = sorted(self.events.keys())
-        return result
-
-
-    @staticmethod
-    def deserialize(data, events):
-        """
-        Convert dict (deserialized from JSON) into an L{Calendar}.
-        """
-
-        calendar = Calendar(None, None, None, None, None)
-        for attr in ("resourceType", "name", "url", "changeToken"):
-            setattr(calendar, attr, u2str(data[attr]))
-        calendar.componentTypes = set(map(u2str, data["componentTypes"]))
-
-        for event in data["events"]:
-            url = urljoin(calendar.url, event)
-            if url in events:
-                calendar.events[event] = events[url]
-            else:
-                # Ughh - an event is missing - force changeToken to empty to trigger full resync
-                calendar.changeToken = ""
-        return calendar
-
-
-
 class BaseClient(object):
     """
     Base interface for all simulated clients.
@@ -262,7 +86,7 @@
     _client_id = None   # Unique id for the client itself
 
 
-    def _setEvent(self, href, event):
+    def _cacheEvent(self, href, event):
         """
         Cache the provided event
         """
@@ -271,7 +95,7 @@
         self._calendars[calendar + '/'].events[basePath] = event
 
 
-    def _removeEvent(self, href):
+    def _invalidateEvent(self, href):
         """
         Remove event from local cache.
         """
@@ -281,6 +105,21 @@
         del self._calendars[calendar + '/'].events[basePath]
 
 
+    def _cacheCalendar(self, href, calendar):
+        """
+        Cache the provided L{Calendar}
+        """
+        self._calendars[href] = calendar
+
+
+    def _invalidateCalendar(self, href):
+        """
+        Remove calendar from the local cache
+        """
+        if href in self._calendars:
+            del self._calendars[href]
+
+
     def addEvent(self, href, calendar):
         """
         Called when a profile needs to add an event (no scheduling).
@@ -324,46 +163,29 @@
         """
         raise NotImplementedError("%r does not implement changeEventAttendee" % (self.__class__,))
 
+    def addCalendar(self, href, calendar):
+        """
+        Called when a profile needs to add a new calendar.
+        """
+        raise NotImplementedError("%r does not implement addCalendar" % (self.__class__,))
 
+    def changeCalendar(self, href, calendar):
+        """
+        Called when a profile needs to change a calendar.
+        """
+        raise NotImplementedError("%r does not implement changeCalendar" % (self.__class__,))
 
-class _PubSubClientFactory(PubSubClientFactory):
-    """
-    Factory for XMPP pubsub functionality.
-    """
-    def __init__(self, client, *args, **kwargs):
-        PubSubClientFactory.__init__(self, *args, **kwargs)
-        self._client = client
 
+    def deleteCalendar(self, href):
+        """
+        Called when a profile needs to delete a calendar.
+        """
+        raise NotImplementedError("%r does not implement deleteCalendar" % (self.__class__,))
 
-    def initFailed(self, reason):
-        print('XMPP initialization failed', reason)
 
 
-    def authFailed(self, reason):
-        print('XMPP Authentication failed', reason)
 
 
-    def handleMessageEventItems(self, iq):
-        item = iq.firstChildElement().firstChildElement()
-        if item:
-            node = item.getAttribute("node")
-            if node:
-                url, _ignore_name, _ignore_kind = self.nodes.get(node, (None, None, None))
-                if url is not None:
-                    self._client._checkCalendarsForEvents(url, push=True)
-
-
-
-class WebClientContextFactory(ClientContextFactory):
-    """
-    A web context factory which ignores the hostname and port and does no
-    certificate verification.
-    """
-    def getContext(self, hostname, port):
-        return ClientContextFactory.getContext(self)
-
-
-
 class BaseAppleClient(BaseClient):
     """
     Implementation of common OS X/iOS client behavior.
@@ -371,7 +193,8 @@
 
     _client_type = "Generic"
 
-    USER_AGENT = None   # Override this for specific clients
+    # Override this for specific clients
+    USER_AGENT = None
 
     # The default interval, used if none is specified in external
     # configuration.
@@ -383,9 +206,6 @@
     # Override and turn on if client supports Sync REPORT
     _SYNC_REPORT = False
 
-    # Override and turn on if client syncs using time-range queries
-    _SYNC_TIMERANGE = False
-
     # Override and turn off if client does not support attendee lookups
     _ATTENDEE_LOOKUPS = True
 
@@ -419,32 +239,22 @@
         self,
         reactor,
         root,
-        principalPathTemplate,
         serializePath,
         record,
         auth,
         title=None,
         calendarHomePollInterval=None,
-        supportPush=True,
         supportAmpPush=True,
         ampPushHost=None,
         ampPushPort=62311,
     ):
-
         self._client_id = str(uuid4())
-
         self.reactor = reactor
 
-        # The server might use gzip encoding
-        agent = Agent(
-            self.reactor,
-            contextFactory=_DeprecatedToCurrentPolicyForHTTPS(WebClientContextFactory()),
+        self.requester = Requester(
+            root, self.getDefaultHeaders(), title,
+            record.uid, self._client_id, auth, self.reactor
         )
-        agent = ContentDecoderAgent(agent, [("gzip", GzipDecoder)])
-        self.agent = AuthHandlerAgent(agent, auth)
-
-        self.root = root
-        self.principalPathTemplate = principalPathTemplate
         self.record = record
 
         self.title = title if title else self._client_type
@@ -453,208 +263,62 @@
             calendarHomePollInterval = self.CALENDAR_HOME_POLL_INTERVAL
         self.calendarHomePollInterval = calendarHomePollInterval
 
-        self.supportPush = supportPush
+        if supportAmpPush:
+            if ampPushHost is None:
+                ampPushHost = urlparse(root)[1].split(":")[0]
+            self.monitor = PushMonitor(self.reactor, ampPushHost, ampPushPort, self.updateCalendarHomeFromPush)
+        else:
+            self.monitor = None
 
-        self.supportAmpPush = supportAmpPush
-        if ampPushHost is None:
-            ampPushHost = urlparse(self.root)[1].split(":")[0]
-        self.ampPushHost = ampPushHost
-        self.ampPushPort = ampPushPort
-
         self.serializePath = serializePath
 
         self.supportSync = self._SYNC_REPORT
 
-        # Keep track of the calendars on this account, keys are
-        # Calendar URIs, values are Calendar instances.
-        self._calendars = {}
-
         # The principalURL found during discovery
         self.principalURL = None
 
         # The principal collection found during startup
         self.principalCollection = None
 
+        # Keep track of the calendars on this account, keys are
+        # Calendar URIs, values are Calendar instances.
+        self._calendars = {}
+
         # Keep track of the events on this account, keys are event
         # URIs (which are unambiguous across different calendars
         # because they start with the uri of the calendar they are
         # part of), values are Event instances.
         self._events = {}
 
-        # Keep track of which calendar homes are being polled
-        self._checking = set()
-
-        # Keep track of XMPP parameters for calendar homes we encounter.  This
-        # dictionary has calendar home URLs as keys and XMPPPush instances as
-        # values.
-        self.xmpp = {}
-
-        self.ampPushKeys = {}
-
-        # Keep track of push factories so we can unsubscribe at shutdown
-        self._pushFactories = []
-
         # Allow events to go out into the world.
         self.catalog = {
-            "eventChanged": Periodical(),
+            "eventChanged": Publisher(),
         }
 
+        self._checking = set()
 
-    def _addDefaultHeaders(self, headers):
-        """
-        Add the clients default set of headers to ones being used in a request.
-        Default is to add User-Agent, sub-classes should override to add other
-        client specific things, Accept etc.
-        """
-        headers.setRawHeaders('User-Agent', [self.USER_AGENT])
-
-
-    @inlineCallbacks
-    def _request(self, expectedResponseCodes, method, url, headers=None, body=None, method_label=None):
-        """
-        Execute a request and check against the expected response codes.
-        """
-        if type(expectedResponseCodes) is int:
-            expectedResponseCodes = (expectedResponseCodes,)
-        if headers is None:
-            headers = Headers({})
-        self._addDefaultHeaders(headers)
-        msg(
-            type="request",
-            method=method_label if method_label else method,
-            url=url,
-            user=self.record.uid,
-            client_type=self.title,
-            client_id=self._client_id,
-        )
-
-        before = self.reactor.seconds()
-        response = yield self.agent.request(method, url, headers, body)
-
-        # XXX This is time to receive response headers, not time
-        # to receive full response.  Should measure the latter, if
-        # not both.
-        after = self.reactor.seconds()
-
-        success = response.code in expectedResponseCodes
-
-        msg(
-            type="response",
-            success=success,
-            method=method_label if method_label else method,
-            headers=headers,
-            body=body,
-            code=response.code,
-            user=self.record.uid,
-            client_type=self.title,
-            client_id=self._client_id,
-            duration=(after - before),
-            url=url,
-        )
-
-        if success:
-            returnValue(response)
-
-        raise IncorrectResponseCode(expectedResponseCodes, response)
-
-
-    def _parseMultiStatus(self, response, otherTokens=False):
-        """
-        Parse a <multistatus> - might need to return other top-level elements
-        in the response - e.g. DAV:sync-token
-        I{PROPFIND} request for the principal URL.
-
-        @type response: C{str}
-        @rtype: C{cls}
-        """
-        parser = PropFindParser()
-        parser.parseData(response)
-        if otherTokens:
-            return (parser.getResults(), parser.getOthers(),)
-        else:
-            return parser.getResults()
-
     _CALENDAR_TYPES = set([
         caldavxml.calendar,
         caldavxml.schedule_inbox,
     ])
 
-    @inlineCallbacks
-    def _propfind(self, url, body, depth='0', allowedStatus=(MULTI_STATUS,), method_label=None):
-        """
-        Issue a PROPFIND on the chosen URL
-        """
-        hdrs = Headers({'content-type': ['text/xml']})
-        if depth is not None:
-            hdrs.addRawHeader('depth', depth)
-        response = yield self._request(
-            allowedStatus,
-            'PROPFIND',
-            self.root + url.encode('utf-8'),
-            hdrs,
-            StringProducer(body),
-            method_label=method_label,
-        )
+    def getDefaultHeaders(self):
+        return {
+            'User-Agent': [self.USER_AGENT],
+            'Accept': ['*/*'],
+            'Accept-Language': ['en-us'],
+            'Accept-Encoding': ['gzip,deflate'],
+            'Connection': ['keep-alive']
+        }
 
-        body = yield readBody(response)
-        result = self._parseMultiStatus(body) if response.code == MULTI_STATUS else None
-
-        returnValue((response, result,))
-
-
     @inlineCallbacks
-    def _proppatch(self, url, body, method_label=None):
-        """
-        Issue a PROPPATCH on the chosen URL
-        """
-        hdrs = Headers({'content-type': ['text/xml']})
-        response = yield self._request(
-            (OK, MULTI_STATUS,),
-            'PROPPATCH',
-            self.root + url.encode('utf-8'),
-            hdrs,
-            StringProducer(body),
-            method_label=method_label,
-        )
-        if response.code == MULTI_STATUS:
-            body = yield readBody(response)
-            result = self._parseMultiStatus(body)
-            returnValue(result)
-        else:
-            returnValue(None)
-
-
-    @inlineCallbacks
-    def _report(self, url, body, depth='0', allowedStatus=(MULTI_STATUS,), otherTokens=False, method_label=None):
-        """
-        Issue a REPORT on the chosen URL
-        """
-        hdrs = Headers({'content-type': ['text/xml']})
-        if depth is not None:
-            hdrs.addRawHeader('depth', depth)
-        response = yield self._request(
-            allowedStatus,
-            'REPORT',
-            self.root + url.encode('utf-8'),
-            hdrs,
-            StringProducer(body),
-            method_label=method_label,
-        )
-
-        body = yield readBody(response)
-        result = self._parseMultiStatus(body, otherTokens) if response.code == MULTI_STATUS else None
-
-        returnValue(result)
-
-
-    @inlineCallbacks
     def _startupPropfindWellKnown(self):
         """
         Issue a PROPFIND on the /.well-known/caldav/ URL
         """
 
         location = "/.well-known/caldav/"
-        response, result = yield self._propfind(
+        response, result = yield self.requester.propfind(
             location,
             self._STARTUP_WELL_KNOWN,
             allowedStatus=(MULTI_STATUS, MOVED_PERMANENTLY, FOUND,),
@@ -665,7 +329,7 @@
         if response.code in (MOVED_PERMANENTLY, FOUND,):
             location = response.headers.getRawHeaders("location")[0]
             location = urlsplit(location)[2]
-            response, result = yield self._propfind(
+            response, result = yield self.requester.propfind(
                 location,
                 self._STARTUP_WELL_KNOWN,
                 allowedStatus=(MULTI_STATUS),
@@ -681,8 +345,8 @@
         Issue a PROPFIND on the /principals/users/<uid> URL to retrieve
         the /principals/__uids__/<guid> principal URL
         """
-        principalPath = self.principalPathTemplate % (user,)
-        _ignore_response, result = yield self._propfind(
+        principalPath = '/principals/users/%s' % (user,)
+        _ignore_response, result = yield self.requester.propfind(
             principalPath,
             self._STARTUP_PRINCIPAL_PROPFIND_INITIAL,
             method_label="PROPFIND{find-principal}",
@@ -697,7 +361,7 @@
         user and return a L{Principal} instance constructed from the
         response.
         """
-        _ignore_response, result = yield self._propfind(
+        _ignore_response, result = yield self.requester.propfind(
             self.principalURL,
             self._STARTUP_PRINCIPAL_PROPFIND,
             method_label="PROPFIND{principal}",
@@ -709,7 +373,7 @@
         """
         Issue a principal-search-property-set REPORT against the chosen URL
         """
-        return self._report(
+        return self.requester.report(
             principalCollectionSet,
             self._STARTUP_PRINCIPALS_REPORT,
             allowedStatus=(OK,),
@@ -718,28 +382,14 @@
 
 
     @inlineCallbacks
-    def _calendarHomePropfind(self, calendarHomeSet):
-        """
-        Do the poll Depth:1 PROPFIND on the calendar home.
-        """
-        if not calendarHomeSet.endswith('/'):
-            calendarHomeSet = calendarHomeSet + '/'
-        _ignore_response, result = yield self._propfind(
-            calendarHomeSet,
-            self._POLL_CALENDARHOME_PROPFIND,
-            depth='1',
-            method_label="PROPFIND{home}",
-        )
-        calendars = self._extractCalendars(result, calendarHomeSet)
-        returnValue((calendars, result,))
-
-
-    @inlineCallbacks
     def _extractPrincipalDetails(self):
         # Using the actual principal URL, retrieve principal information
+        # XXX We could be recording more information here
         principal = yield self._principalPropfind()
 
         hrefs = principal.getHrefProperties()
+        # from pprint import pprint
+        # pprint(hrefs)
 
         # Remember our outbox and ignore notifications
         self.outbox = hrefs[caldavxml.schedule_outbox_URL].toString()
@@ -767,14 +417,115 @@
 
         returnValue(principal)
 
+    def startup(self):
+        """
+        Overridden by subclasses of BaseAppleClient.
+        """
+        raise NotImplementedError
 
+    def calendarCheckLoop(self, calendarHome):
+        """
+        Periodically check the calendar home for changes to calendars.
+        """
+        pollCalendarHome = LoopingCall(
+            self.checkCalendarsForEvents, calendarHome)
+        return pollCalendarHome.start(self.calendarHomePollInterval, now=False)
+
+    ### TODO this doesn't seem to always work
+    @inlineCallbacks
+    def updateCalendarHomeFromPush(self, calendarHomeSet):
+        """
+        Emulate the client behavior upon receiving a notification that the
+        given calendar home has changed.
+        """
+        # Todo - ensure that the self._checking set is properly cleared even if there is an error
+        self._checking.add(calendarHomeSet)
+        result = yield self._poll(calendarHomeSet, firstTime=False)
+
+        # Todo - should this be a returnValue?
+        yield self._newOperation("push", result)
+
+    @inlineCallbacks
+    def checkCalendarsForEvents(self, calendarHomeSet, firstTime=False):
+        """
+        The actions a client does when polling for changes, or in response to a
+        push notification of a change. There are some actions done on the first poll
+        we should emulate.
+        """
+
+        result = True
+        try:
+            result = yield self._newOperation("poll", self._poll(calendarHomeSet, firstTime))
+        finally:
+            if result:
+                try:
+                    self._checking.remove(calendarHomeSet)
+                except KeyError:
+                    pass
+        returnValue(result)
+
+    """
+    REFRESH UTILITIES
+    """
+
+    @inlineCallbacks
+    def _poll(self, calendarHomeSet, firstTime):
+        if calendarHomeSet in self._checking:
+            returnValue(False)
+        self._checking.add(calendarHomeSet)
+
+        calendars, results = yield self._calendarHomePropfind(calendarHomeSet)
+
+        # First time operations
+        if firstTime:
+            yield self._pollFirstTime1(results[calendarHomeSet], calendars)
+
+        # Normal poll
+        for cal in calendars:
+            newToken = cal.changeToken
+            if cal.url not in self._calendars:
+                # Calendar seen for the first time - reload it
+                self._calendars[cal.url] = cal
+                cal.changeToken = ""
+                yield self._updateCalendar(self._calendars[cal.url], newToken)
+            elif self._calendars[cal.url].changeToken != newToken:
+                # Calendar changed - reload it
+                yield self._updateCalendar(self._calendars[cal.url], newToken)
+
+        # When there is no sync REPORT, clients have to do a full PROPFIND
+        # on the notification collection because there is no ctag
+        if self.notificationURL is not None and not self.supportSync:
+            yield self._notificationPropfind(self.notificationURL)
+            yield self._notificationChangesPropfind(self.notificationURL)
+
+        # One time delegate expansion
+        if firstTime:
+            # yield self._pollFirstTime2()
+            pass
+
+        returnValue(True)
+
+    @inlineCallbacks
+    def _calendarHomePropfind(self, calendarHomeSet):
+        """
+        Do the poll Depth:1 PROPFIND on the calendar home.
+        """
+        if not calendarHomeSet.endswith('/'):
+            calendarHomeSet = calendarHomeSet + '/'
+        _ignore_response, result = yield self.requester.propfind(
+            calendarHomeSet,
+            self._POLL_CALENDARHOME_PROPFIND,
+            depth='1',
+            method_label="PROPFIND{home}",
+        )
+        calendars = self._extractCalendars(result, calendarHomeSet)
+        returnValue((calendars, result,))
+
+
     def _extractCalendars(self, results, calendarHome=None):
         """
         Parse a calendar home PROPFIND response and create local state
         representing the calendars it contains.
-
-        If XMPP push is enabled, also look for and record information about
-        that from the response.
         """
         calendars = []
         for href in results:
@@ -787,19 +538,9 @@
                 except KeyError:
                     pass
                 else:
-                    if pushkey:
-                        self.ampPushKeys[href] = pushkey
+                    if pushkey and self.monitor:
+                        self.monitor.addPushkey(pushkey, href)
 
-                try:
-                    server = text[csxml.xmpp_server]
-                    uri = text[csxml.xmpp_uri]
-                    pushkey = text[csxml.pushkey]
-                except KeyError:
-                    pass
-                else:
-                    if server and uri:
-                        self.xmpp[href] = XMPPPush(server, uri, pushkey)
-
             nodes = results[href].getNodeProperties()
             for nodeType in nodes[davxml.resourcetype]:
                 if nodeType.tag in self._CALENDAR_TYPES:
@@ -822,6 +563,40 @@
         return calendars
 
 
+
+
+    @inlineCallbacks
+    def _pollFirstTime1(self, homeNode, calendars):
+        # Detect sync report if needed
+        if self.supportSync:
+            nodes = homeNode.getNodeProperties()
+            syncnodes = nodes[davxml.supported_report_set].findall(
+                str(davxml.supported_report) + "/" +
+                str(davxml.report) + "/" +
+                str(davxml.sync_collection)
+            )
+            self.supportSync = len(syncnodes) != 0
+
+        # Patch calendar properties
+        for cal in calendars:
+            if cal.name != "inbox":
+                # yield self.requester.proppatch(
+                #     cal.url,
+                #     self._STARTUP_PROPPATCH_CALENDAR_COLOR,
+                #     method_label="PROPPATCH{calendar}",
+                # )
+                yield self.requester.proppatch(
+                    cal.url,
+                    self._STARTUP_PROPPATCH_CALENDAR_ORDER,
+                    method_label="PROPPATCH{calendar}",
+                )
+                yield self.requester.proppatch(
+                    cal.url,
+                    self._STARTUP_PROPPATCH_CALENDAR_TIMEZONE,
+                    method_label="PROPPATCH{calendar}",
+                )
+
+
     def _updateCalendar(self, calendar, newToken):
         """
         Update the local cached data for a calendar in an appropriate manner.
@@ -843,11 +618,11 @@
         # the sim can fire a PUT between the PROPFIND and when process the removals.
         old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
 
-        _ignore_response, result = yield self._propfind(
+        _ignore_response, result = yield self.requester.propfind(
             calendar.url,
             self._POLL_CALENDAR_PROPFIND_D1,
+            method_label="PROPFIND{calendar}",
             depth='1',
-            method_label="PROPFIND{calendar}"
         )
 
         yield self._updateApplyChanges(calendar, result, old_hrefs)
@@ -871,7 +646,7 @@
         # Get changes from sync REPORT (including the other nodes at the top-level
         # which will have the new sync token.
         fullSync = not calendar.changeToken
-        result = yield self._report(
+        result = yield self.requester.report(
             calendar.url,
             self._POLL_CALENDAR_SYNC_REPORT % {'sync-token': calendar.changeToken},
             depth='1',
@@ -882,7 +657,7 @@
         if result is None:
             if not fullSync:
                 fullSync = True
-                result = yield self._report(
+                result = yield self.requester.report(
                     calendar.url,
                     self._POLL_CALENDAR_SYNC_REPORT % {'sync-token': ''},
                     depth='1',
@@ -908,13 +683,13 @@
             # Differentiate a remove vs new/update result
             if result[responseHref].getStatus() / 100 == 2:
                 if responseHref not in self._events:
-                    self._setEvent(responseHref, Event(self.serializeLocation(), responseHref, None))
+                    self._cacheEvent(responseHref, Event(self.serializeLocation(), responseHref, None))
 
                 event = self._events[responseHref]
                 if event.etag != etag:
                     changed.append(responseHref)
             elif result[responseHref].getStatus() == 404:
-                self._removeEvent(responseHref)
+                self._invalidateEvent(responseHref)
 
         yield self._updateChangedEvents(calendar, changed)
 
@@ -923,7 +698,7 @@
             # Detect removed items and purge them
             remove_hrefs = old_hrefs - set(changed)
             for href in remove_hrefs:
-                self._removeEvent(href)
+                self._invalidateEvent(href)
 
         # Now update calendar to the new token taken from the report
         for node in others:
@@ -954,7 +729,7 @@
                 continue
 
             if responseHref not in self._events:
-                self._setEvent(responseHref, Event(self.serializeLocation(), responseHref, None))
+                self._cacheEvent(responseHref, Event(self.serializeLocation(), responseHref, None))
 
             event = self._events[responseHref]
             if event.etag != etag:
@@ -966,7 +741,7 @@
         # Detect removed items and purge them
         remove_hrefs = old_hrefs - set(all_hrefs)
         for href in remove_hrefs:
-            self._removeEvent(href)
+            self._invalidateEvent(href)
 
 
     @inlineCallbacks
@@ -982,9 +757,9 @@
             changed = changed[self.MULTIGET_BATCH_SIZE:]
 
             multistatus = yield self._eventReport(calendar.url, batchedHrefs)
-            for responseHref in batchedHrefs:
+            for href in batchedHrefs:
                 try:
-                    res = multistatus[responseHref]
+                    res = multistatus[href]
                 except KeyError:
                     # Resource might have been deleted
                     continue
@@ -996,18 +771,10 @@
                     except KeyError:
                         scheduleTag = None
                     body = text[caldavxml.calendar_data]
-                    self.eventChanged(responseHref, etag, scheduleTag, body)
+                    component = Component.fromString(body)
+                    self._updateEventCache(href, etag, scheduleTag, component)
 
 
-    def eventChanged(self, href, etag, scheduleTag, body):
-        event = self._events[href]
-        event.etag = etag
-        if scheduleTag is not None:
-            event.scheduleTag = scheduleTag
-        event.component = Component.fromString(body)
-        self.catalog["eventChanged"].issue(href)
-
-
     def _eventReport(self, calendar, events):
         # Next do a REPORT on events that might have information
         # we don't know about.
@@ -1021,7 +788,7 @@
         if len(events) > 75:
             label_suffix = "huge"
 
-        return self._report(
+        return self.requester.report(
             calendar,
             self._POLL_CALENDAR_MULTIGET_REPORT % {'hrefs': hrefs},
             depth=None,
@@ -1030,101 +797,8 @@
 
 
     @inlineCallbacks
-    def _checkCalendarsForEvents(self, calendarHomeSet, firstTime=False, push=False):
-        """
-        The actions a client does when polling for changes, or in response to a
-        push notification of a change. There are some actions done on the first poll
-        we should emulate.
-        """
-
-        result = True
-        try:
-            result = yield self._newOperation("push" if push else "poll", self._poll(calendarHomeSet, firstTime))
-        finally:
-            if result:
-                try:
-                    self._checking.remove(calendarHomeSet)
-                except KeyError:
-                    pass
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def _poll(self, calendarHomeSet, firstTime):
-        if calendarHomeSet in self._checking:
-            returnValue(False)
-        self._checking.add(calendarHomeSet)
-
-        calendars, results = yield self._calendarHomePropfind(calendarHomeSet)
-
-        # First time operations
-        if firstTime:
-            yield self._pollFirstTime1(results[calendarHomeSet], calendars)
-
-        # Normal poll
-        for cal in calendars:
-            newToken = cal.changeToken
-            if cal.url not in self._calendars:
-                # Calendar seen for the first time - reload it
-                self._calendars[cal.url] = cal
-                cal.changeToken = ""
-                yield self._updateCalendar(self._calendars[cal.url], newToken)
-            elif self._calendars[cal.url].changeToken != newToken:
-                # Calendar changed - reload it
-                yield self._updateCalendar(self._calendars[cal.url], newToken)
-
-        # When there is no sync REPORT, clients have to do a full PROPFIND
-        # on the notification collection because there is no ctag
-        if self.notificationURL is not None and not self.supportSync:
-            yield self._notificationPropfind(self.notificationURL)
-            yield self._notificationChangesPropfind(self.notificationURL)
-
-        # One time delegate expansion
-        if firstTime:
-            yield self._pollFirstTime2()
-
-        returnValue(True)
-
-
-    @inlineCallbacks
-    def _pollFirstTime1(self, homeNode, calendars):
-        # Detect sync report if needed
-        if self.supportSync:
-            nodes = homeNode.getNodeProperties()
-            syncnodes = nodes[davxml.supported_report_set].findall(
-                str(davxml.supported_report) + "/" +
-                str(davxml.report) + "/" +
-                str(davxml.sync_collection)
-            )
-            self.supportSync = len(syncnodes) != 0
-
-        # Patch calendar properties
-        for cal in calendars:
-            if cal.name != "inbox":
-                yield self._proppatch(
-                    cal.url,
-                    self._STARTUP_PROPPATCH_CALENDAR_COLOR,
-                    method_label="PROPPATCH{calendar}",
-                )
-                yield self._proppatch(
-                    cal.url,
-                    self._STARTUP_PROPPATCH_CALENDAR_ORDER,
-                    method_label="PROPPATCH{calendar}",
-                )
-                yield self._proppatch(
-                    cal.url,
-                    self._STARTUP_PROPPATCH_CALENDAR_TIMEZONE,
-                    method_label="PROPPATCH{calendar}",
-                )
-
-
-    def _pollFirstTime2(self):
-        return self._principalExpand(self.principalURL)
-
-
-    @inlineCallbacks
     def _notificationPropfind(self, notificationURL):
-        _ignore_response, result = yield self._propfind(
+        _ignore_response, result = yield self.requester.propfind(
             notificationURL,
             self._POLL_NOTIFICATION_PROPFIND,
             method_label="PROPFIND{notification}",
@@ -1134,7 +808,7 @@
 
     @inlineCallbacks
     def _notificationChangesPropfind(self, notificationURL):
-        _ignore_response, result = yield self._propfind(
+        _ignore_response, result = yield self.requester.propfind(
             notificationURL,
             self._POLL_NOTIFICATION_PROPFIND_D1,
             depth='1',
@@ -1142,10 +816,12 @@
         )
         returnValue(result)
 
+    def _pollFirstTime2(self):
+        return self._principalExpand(self.principalURL)
 
     @inlineCallbacks
     def _principalExpand(self, principalURL):
-        result = yield self._report(
+        result = yield self.requester.report(
             principalURL,
             self._STARTUP_PRINCIPAL_EXPAND,
             depth=None,
@@ -1154,19 +830,7 @@
         returnValue(result)
 
 
-    def startup(self):
-        raise NotImplementedError
 
-
-    def _calendarCheckLoop(self, calendarHome):
-        """
-        Periodically check the calendar home for changes to calendars.
-        """
-        pollCalendarHome = LoopingCall(
-            self._checkCalendarsForEvents, calendarHome)
-        return pollCalendarHome.start(self.calendarHomePollInterval, now=False)
-
-
     @inlineCallbacks
     def _newOperation(self, label, deferred):
         before = self.reactor.seconds()
@@ -1205,55 +869,7 @@
         returnValue(result)
 
 
-    def _monitorPubSub(self, home, params):
-        """
-        Start monitoring the
-        """
-        host, port = params.server.split(':')
-        port = int(port)
-
-        service, _ignore_stuff = params.uri.split('?')
-        service = service.split(':', 1)[1]
-
-        # XXX What is the domain of the 2nd argument supposed to be?  The
-        # hostname we use to connect, or the same as the email address in the
-        # user record?
-        factory = _PubSubClientFactory(
-            self, "%s@%s" % (self.record.uid, host),
-            self.record.password, service,
-            {params.pushkey: (home, home, "Calendar home")}, False,
-            sigint=False)
-        self._pushFactories.append(factory)
-        connect(GAIEndpoint(self.reactor, host, port), factory)
-
-
-    def _receivedPush(self, inboundID, dataChangedTimestamp, priority=5):
-        for href, id in self.ampPushKeys.iteritems():
-            if inboundID == id:
-                self._checkCalendarsForEvents(href, push=True)
-                break
-        else:
-            # somehow we are not subscribed to this id
-            pass
-
-
-    def _monitorAmpPush(self, home, pushKeys):
-        """
-        Start monitoring for AMP-based push notifications
-        """
-        subscribeToIDs(
-            self.ampPushHost, self.ampPushPort, pushKeys,
-            self._receivedPush, self.reactor
-        )
-
-
     @inlineCallbacks
-    def _unsubscribePubSub(self):
-        for factory in self._pushFactories:
-            yield factory.unsubscribeAll()
-
-
-    @inlineCallbacks
     def run(self):
         """
         Emulate a CalDAV client.
@@ -1265,37 +881,30 @@
             calendarHome = hrefs[caldavxml.calendar_home_set].toString()
             if calendarHome is None:
                 raise MissingCalendarHome
-            yield self._checkCalendarsForEvents(calendarHome, firstTime=True)
+            yield self.checkCalendarsForEvents(calendarHome, firstTime=True)
             returnValue(calendarHome)
         calendarHome = yield self._newOperation("startup: %s" % (self.title,), startup())
-
         self.started = True
 
-        # Start monitoring PubSub notifications, if possible.
-        # _checkCalendarsForEvents populates self.xmpp if it finds
-        # anything.
-        if self.supportPush and calendarHome in self.xmpp:
-            self._monitorPubSub(calendarHome, self.xmpp[calendarHome])
+        # Start monitoring AMP push notifications, if possible
+        if self.monitor and self.monitor.isSubscribedTo(calendarHome):
+            yield self.monitor.begin()
             # Run indefinitely.
             yield Deferred()
-        elif self.supportAmpPush and calendarHome in self.ampPushKeys:
-            pushKeys = self.ampPushKeys.values()
-            self._monitorAmpPush(calendarHome, pushKeys)
-            # Run indefinitely.
-            yield Deferred()
         else:
             # This completes when the calendar home poll loop completes, which
             # currently it never will except due to an unexpected error.
-            yield self._calendarCheckLoop(calendarHome)
+            yield self.calendarCheckLoop(calendarHome)
 
 
     def stop(self):
         """
         Called before connections are closed, giving a chance to clean up
         """
-
         self.serialize()
-        return self._unsubscribePubSub()
+        if not self.monitor:
+            return succeed(None)
+        return self.monitor.end()
 
 
     def serializeLocation(self):
@@ -1387,94 +996,67 @@
         )
         return organizer
 
+    def _getEventSizeDescription(self, numAttendees):
+        if numAttendees > 75:
+            return "huge"
+        if numAttendees > 20:
+            return "large"
+        if numAttendees > 5:
+            return "medium"
+        return "small"
 
+    """ literally wtf is this event stuff
+submitEvent(event: Event, )
+
+"""
     @inlineCallbacks
     def addEventAttendee(self, href, attendee):
+        individual = attendee.parameterValue('CUTYPE') == 'INDIVIDUAL'
 
         event = self._events[href]
         component = event.component
+        vevent = component.mainComponent()
 
+        query = attendee.parameterValue('CN')
+
+        from pprint import pprint
         # Trigger auto-complete behavior
-        yield self._attendeeAutoComplete(component, attendee)
+        matchingPrincipals = yield self._principalSearchReport(query, isAttendeeSearch=individual)
+        # for k, v in matchingPrincipals.items():
+            # pprint(k)
+            # for prop, val in v.getNodeProperties().items():
+            #     # print("%s %s" % (prop, val.__dict__))
+            #     for child in val._children:
+            #         # print(child.text)
 
-        # If the event has no attendees, add ourselves as an attendee.
-        attendees = list(component.mainComponent().properties('ATTENDEE'))
-        if len(attendees) == 0:
-            # First add ourselves as a participant and as the
-            # organizer.  In the future for this event we should
-            # already have those roles.
-            component.mainComponent().addProperty(self._makeSelfOrganizer())
-            component.mainComponent().addProperty(self._makeSelfAttendee())
-        attendees.append(attendee)
-        component.mainComponent().addProperty(attendee)
+        uuids = []
 
-        label_suffix = "small"
-        if len(attendees) > 5:
-            label_suffix = "medium"
-        if len(attendees) > 20:
-            label_suffix = "large"
-        if len(attendees) > 75:
-            label_suffix = "huge"
+        for principal_url, propfindresult in matchingPrincipals.items():
+            props = propfindresult.getNodeProperties()
+            for cuaddr in props.get(caldavxml.calendar_user_address_set):
+                # print(cuaddr)
+                uuids.append(cuaddr.text)
+                break
 
-        # At last, upload the new event definition
-        response = yield self._request(
-            (NO_CONTENT, PRECONDITION_FAILED,),
-            'PUT',
-            self.root + href.encode('utf-8'),
-            Headers({
-                    'content-type': ['text/calendar'],
-                    'if-match': [event.etag]}),
-            StringProducer(component.getTextWithTimezones(includeTimezones=True)),
-            method_label="PUT{organizer-%s}" % (label_suffix,)
-        )
+        # print(uuids)
 
-        # Finally, re-retrieve the event to update the etag
-        yield self._updateEvent(response, href)
 
+        start = vevent.getStartDateUTC()
+        end = vevent.getEndDateUTC()
 
-    @inlineCallbacks
-    def _attendeeAutoComplete(self, component, attendee):
+        yield self.requestAvailability(start, end, uuids)
 
-        if self._ATTENDEE_LOOKUPS:
-            # Temporarily use some non-test names (some which will return
-            # many results, and others which will return fewer) because the
-            # test account names are all too similar
-            # name = attendee.parameterValue('CN').encode("utf-8")
-            # prefix = name[:4].lower()
-            prefix = random.choice([
-                "chris", "cyru", "dre", "eric", "morg",
-                "well", "wilfr", "witz"
-            ])
+        # # Do free-busy lookups
+        # if individual:
+        #     # When adding individual attendees, we only look up the availability
+        #     # of the specific attendee
+        #     yield self.checkAvailability()
+        # else:
+        #     # When adding a location, we look up the availability of each location
+        #     # returned by the principal search.
+        #     yield self.checkAvailability()
 
-            email = attendee.value()
-            if email.startswith("mailto:"):
-                email = email[7:]
-            elif attendee.hasParameter('EMAIL'):
-                email = attendee.parameterValue('EMAIL').encode("utf-8")
 
-            # First try to discover some names to supply to the
-            # auto-completion
-            yield self._report(
-                self.principalCollection,
-                self._USER_LIST_PRINCIPAL_PROPERTY_SEARCH % {
-                    'displayname': prefix,
-                    'email': prefix,
-                    'firstname': prefix,
-                    'lastname': prefix,
-                },
-                depth=None,
-                method_label="REPORT{psearch}",
-            )
-
-            # Now learn about the attendee's availability
-            yield self.requestAvailability(
-                component.mainComponent().getStartDateUTC(),
-                component.mainComponent().getEndDateUTC(),
-                [self.email, u'mailto:' + email],
-                [component.resourceUID()]
-            )
-
-
     @inlineCallbacks
     def changeEventAttendee(self, href, oldAttendee, newAttendee):
         event = self._events[href]
@@ -1483,149 +1065,197 @@
         # Change the event to have the new attendee instead of the old attendee
         component.mainComponent().removeProperty(oldAttendee)
         component.mainComponent().addProperty(newAttendee)
-        okCodes = NO_CONTENT
-        headers = Headers({
-            'content-type': ['text/calendar'],
-        })
+
+        headers = Headers()
         if event.scheduleTag is not None:
             headers.addRawHeader('if-schedule-tag-match', event.scheduleTag)
-            okCodes = (NO_CONTENT, PRECONDITION_FAILED,)
-
+        event.component = component
         attendees = list(component.mainComponent().properties('ATTENDEE'))
-        label_suffix = "small"
-        if len(attendees) > 5:
-            label_suffix = "medium"
-        if len(attendees) > 20:
-            label_suffix = "large"
-        if len(attendees) > 75:
-            label_suffix = "huge"
+        label_suffix = self._getEventSizeDescription(len(attendees))
+        method_label = "PUT{attendee-%s}" % (label_suffix,)
 
-        response = yield self._request(
-            okCodes,
-            'PUT',
-            self.root + href.encode('utf-8'),
-            headers, StringProducer(component.getTextWithTimezones(includeTimezones=True)),
-            method_label="PUT{attendee-%s}" % (label_suffix,),
-        )
+        yield self.putEvent(href, event, headers=headers, method_label=method_label, new=False)
 
-        # Finally, re-retrieve the event to update the etag
-        yield self._updateEvent(response, href)
 
-
     @inlineCallbacks
-    def deleteEvent(self, href):
+    def addInvite(self, event):
         """
-        Issue a DELETE for the given URL and remove local state
-        associated with that event.
+        Add an event that is an invite - i.e., has attendees. Presumably the appropriate principal searches and
+        free-busy lookups have already been accounted for (in addEventAttendee)
         """
+        vevent = event.component.mainComponent()
+        # If the event has no attendees, add ourselves as an attendee.
+        attendees = list(vevent.properties('ATTENDEE'))
+        if len(attendees) == 0:
+            # First add ourselves as a participant and as the
+            # organizer.  In the future for this event we should
+            # already have those roles.
+            vevent.addProperty(self._makeSelfOrganizer())
+            vevent.addProperty(self._makeSelfAttendee())
 
-        self._removeEvent(href)
+        label_suffix = self._getEventSizeDescription(len(attendees))
+        method_label = "PUT{organizer-%s}" % (label_suffix,)
 
-        response = yield self._request(
-            NO_CONTENT,
-            'DELETE',
-            self.root + href.encode('utf-8'),
-            method_label="DELETE{event}",
-        )
-        returnValue(response)
+        yield self.updateEvent(event, method_label=method_label)
 
 
     @inlineCallbacks
-    def addEvent(self, href, component, invite=False):
+    def addEvent(self, href, event):
+        """
+        client.addEvent(
+            Event e
+        """
         headers = Headers({
-            'content-type': ['text/calendar'],
+            'if-none-match': ['*']
         })
+        yield self.putEvent(
+            href,
+            event,
+            headers=headers,
+            method_label="PUT{event}"
+        )
 
-        attendees = list(component.mainComponent().properties('ATTENDEE'))
-        label_suffix = "small"
-        if len(attendees) > 5:
-            label_suffix = "medium"
-        if len(attendees) > 20:
-            label_suffix = "large"
-        if len(attendees) > 75:
-            label_suffix = "huge"
+    # attendees = list(component.mainComponent().properties('ATTENDEE'))
+    # label_suffix = self._getEventSizeDescription(len(attendees))
+    # method_label = "PUT{organizer-%s}" % (label_suffix,) if invite else "PUT{event}"
 
-        response = yield self._request(
-            CREATED,
-            'PUT',
-            self.root + href.encode('utf-8'),
-            headers,
-            StringProducer(component.getTextWithTimezones(includeTimezones=True)),
-            method_label="PUT{organizer-%s}" % (label_suffix,) if invite else "PUT{event}",
-        )
-        self._localUpdateEvent(response, href, component)
+    @inlineCallbacks
+    def updateEvent(self, event, method_label="PUT{event}"):
+        headers = Headers({
+            'if-match': [event.etag]
+        })
+        yield self.putEvent(event.url, event, headers=headers, method_label=method_label)
 
 
     @inlineCallbacks
-    def addInvite(self, href, component):
+    def putEvent(self, href, event, headers=None, method_label=None):
         """
-        Add an event that is an invite - i.e., has attendees. We will do attendee lookups and freebusy
-        checks on each attendee to simulate what happens when an organizer creates a new invite.
+        PUT an event to the server
         """
+        if headers == None:
+            headers = Headers()
+        headers.addRawHeader('content-type', 'text/calendar')
 
-        # Do lookup and free busy of each attendee (not self)
-        attendees = list(component.mainComponent().properties('ATTENDEE'))
-        for attendee in attendees:
-            if attendee.value() in (self.uuid, self.email):
-                continue
-            yield self._attendeeAutoComplete(component, attendee)
+        okCodes = (CREATED, NO_CONTENT, PRECONDITION_FAILED)
 
-        # Now do a normal PUT
-        yield self.addEvent(href, component, invite=True)
+        # At last, upload the new event definition
+        response = yield self.requester.put(
+            okCodes,
+            href,
+            event.component,
+            headers=headers,
+            method_label=method_label
+        )
+        # If the server doesn't return an etag, it has changed the resource
+        # and we need to refetch it
+        if not response.headers.hasHeader('etag'):
+            yield self._refreshEvent(href)
+        else:
+            etag, scheduleTag = self.extractTags(response)
+            yield succeed(self._updateEventCache(href, etag=etag, scheduleTag=scheduleTag, component=event.component))
 
 
     @inlineCallbacks
-    def changeEvent(self, href):
+    def _refreshEvent(self, href):
+        """
+        Issues a GET to the specified href (representing an event that already exists on the server)
+        and uses the response to update local state associated with that event
+        """
+        response = yield self.requester.get(href, method_label="GET{event}")
+        etag, scheduleTag = self.extractTags(response)
+        body = yield readBody(response)
+        component = Component.fromString(body)
+        self._updateEventCache(href, etag=etag, scheduleTag=scheduleTag, component=component)
 
-        event = self._events[href]
-        component = event.component
+    def _updateEventCache(self, href, etag=None, scheduleTag=None, component=None):
+        """
+        Update local state associated with the event at href
+        """
 
-        # At last, upload the new event definition
-        response = yield self._request(
-            (NO_CONTENT, PRECONDITION_FAILED,),
-            'PUT',
-            self.root + href.encode('utf-8'),
-            Headers({
-                'content-type': ['text/calendar'],
-                'if-match': [event.etag]
-            }),
-            StringProducer(component.getTextWithTimezones(includeTimezones=True)),
-            method_label="PUT{update}"
-        )
+        if href in self._events:
+            event = self._events[href]
+        else: # This is a new resource
+            event = Event(self.serializeLocation(), href, None, None)
 
-        # Finally, re-retrieve the event to update the etag
-        yield self._updateEvent(response, href)
+        if etag:
+            event.etag = etag
+        if scheduleTag:
+            event.scheduleTag = scheduleTag
+        if component:
+            event.component = component
 
+        if True: # XXX some other test
+            self.catalog["eventChanged"].issue(href)
+        self._cacheEvent(href, event)
 
-    def _localUpdateEvent(self, response, href, component):
+    @inlineCallbacks
+    def deleteEvent(self, href):
+        """
+        Issue a DELETE for the given URL and remove local state
+        associated with that event.
+        """
+        self._invalidateEvent(href)
+        yield self.requester.delete(href, method_label="DELETE{event}")
+
+    def extractTags(self, response):
         headers = response.headers
         etag = headers.getRawHeaders("etag", [None])[0]
         scheduleTag = headers.getRawHeaders("schedule-tag", [None])[0]
+        return etag, scheduleTag
 
-        event = Event(self.serializeLocation(), href, etag, component)
-        event.scheduleTag = scheduleTag
-        self._setEvent(href, event)
+    # @inlineCallbacks
+    # def _attendeeAutoComplete(self, component, attendee):
 
+    #     if self._ATTENDEE_LOOKUPS:
+    #         # Temporarily use some non-test names (some which will return
+    #         # many results, and others which will return fewer) because the
+    #         # test account names are all too similar
+    #         # name = attendee.parameterValue('CN').encode("utf-8")
+    #         # prefix = name[:4].lower()
+    #         prefix = random.choice([
+    #             "chris", "cyru", "dre", "eric", "morg",
+    #             "well", "wilfr", "witz"
+    #         ])
 
-    def updateEvent(self, href):
-        return self._updateEvent(None, href)
+    #         email = attendee.value()
+    #         if email.startswith("mailto:"):
+    #             email = email[7:]
+    #         elif attendee.hasParameter('EMAIL'):
+    #             email = attendee.parameterValue('EMAIL').encode("utf-8")
 
+    #         # First try to discover some names to supply to the
+    #         # auto-completion
+    #         yield self.requester.report(
+    #             self.principalCollection,
+    #             self._USER_LIST_PRINCIPAL_PROPERTY_SEARCH % {
+    #                 'displayname': prefix,
+    #                 'email': prefix,
+    #                 'firstname': prefix,
+    #                 'lastname': prefix,
+    #             },
+    #             depth=None,
+    #             method_label="REPORT{psearch}",
+    #         )
 
+    #         # Now learn about the attendee's availability
+    #         yield self.requestAvailability(
+    #             component.mainComponent().getStartDateUTC(),
+    #             component.mainComponent().getEndDateUTC(),
+    #             [self.email, u'mailto:' + email],
+    #             [component.resourceUID()]
+    #         )
+
     @inlineCallbacks
-    def _updateEvent(self, ignored, href):
-        response = yield self._request(
-            OK,
-            'GET',
-            self.root + href.encode('utf-8'),
-            method_label="GET{event}",
-        )
-        headers = response.headers
-        etag = headers.getRawHeaders('etag')[0]
-        scheduleTag = headers.getRawHeaders('schedule-tag', [None])[0]
-        body = yield readBody(response)
-        self.eventChanged(href, etag, scheduleTag, body)
+    def _principalSearchReport(self, query, isAttendeeSearch):
+        """ context = attendee if isAttendeeSearch else location """
+        context = "attendee" if isAttendeeSearch else "location"
+        tokens = query.split()
+        search = '\n'.join(["<C:search-token>%s</C:search-token>" % (token, ) for token in tokens])
+        body = self._CALENDARSERVER_PRINCIPAL_SEARCH_REPORT.format(context=context, searchTokens=search)
+        principals = yield self.requester.report('/principals/', body, depth=None)
+        # print("Found some principals:")
+        returnValue(principals)
 
-
     @inlineCallbacks
     def requestAvailability(self, start, end, users, mask=set()):
         """
@@ -1647,8 +1277,6 @@
         @return: A C{Deferred} which fires with a C{dict}.  Keys in the dict
             are user UUIDs (those requested) and values are something else.
         """
-        outbox = self.root + self.outbox
-
         if mask:
             maskStr = u'\r\n'.join(['X-CALENDARSERVER-MASK-UID:' + uid
                                     for uid in mask]) + u'\r\n'
@@ -1672,21 +1300,16 @@
         end = end.getText()
         now = DateTime.getNowUTC().getText()
 
-        label_suffix = "small"
-        if len(users) > 5:
-            label_suffix = "medium"
-        if len(users) > 20:
-            label_suffix = "large"
-        if len(users) > 75:
-            label_suffix = "huge"
+        label_suffix = self._getEventSizeDescription(len(users))
 
-        response = yield self._request(
-            OK, 'POST', outbox,
-            Headers({
-                    'content-type': ['text/calendar'],
-                    'originator': [self.email],
-                    'recipient': [u', '.join(users).encode('utf-8')]}),
-            StringProducer(self._POST_AVAILABILITY % {
+        headers = Headers({
+            'content-type': ['text/calendar'],
+            'originator': [self.email],
+            'recipient': [u', '.join(users).encode('utf-8')]
+        })
+        response = yield self.requester.post(
+            self.outbox,
+            self._POST_AVAILABILITY % {
                 'attendees': attendeeStr,
                 'summary': (u'Availability for %s' % (', '.join(users),)).encode('utf-8'),
                 'organizer': self.email.encode('utf-8'),
@@ -1695,385 +1318,103 @@
                 'start': start,
                 'end': end,
                 'now': now,
-            }),
+            },
+            headers=headers,
             method_label="POST{fb-%s}" % (label_suffix,),
         )
+
         body = yield readBody(response)
         returnValue(body)
 
-
-
-class OS_X_10_6(BaseAppleClient):
-    """
-    Implementation of the OS X 10.6 iCal network behavior.
-
-    Anything OS X 10.6 iCal does on its own, or any particular
-    network behaviors it takes in response to a user action, belong on
-    this class.
-
-    Usage-profile based behaviors ("the user modifies an event every
-    3.2 minutes") belong elsewhere.
-    """
-
-    _client_type = "OS X 10.6"
-
-    USER_AGENT = "DAVKit/4.0.3 (732); CalendarStore/4.0.3 (991); iCal/4.0.3 (1388); Mac OS X/10.6.4 (10F569)"
-
-    # The default interval, used if none is specified in external
-    # configuration.  This is also the actual value used by Snow
-    # Leopard iCal.
-    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
-
-    # The maximum number of resources to retrieve in a single multiget
-    MULTIGET_BATCH_SIZE = 200
-
-    # Override and turn on if client supports Sync REPORT
-    _SYNC_REPORT = False
-
-    # Override and turn on if client syncs using time-range queries
-    _SYNC_TIMERANGE = False
-
-    # Override and turn off if client does not support attendee lookups
-    _ATTENDEE_LOOKUPS = True
-
-    # Request body data
-    _LOAD_PATH = "OS_X_10_6"
-
-    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
-    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
-    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
-    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
-    _STARTUP_PRINCIPAL_EXPAND = loadRequestBody(_LOAD_PATH, 'startup_principal_expand')
-    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
-    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
-    _STARTUP_PROPPATCH_CALENDAR_TIMEZONE = loadRequestBody(_LOAD_PATH, 'startup_calendar_timezone_proppatch')
-
-    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
-    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
-    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
-    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
-    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
-    _POLL_CALENDAR_SYNC_REPORT = None
-    _POLL_NOTIFICATION_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
-    _POLL_NOTIFICATION_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_notification_propfind_d1')
-
-    _USER_LIST_PRINCIPAL_PROPERTY_SEARCH = loadRequestBody(_LOAD_PATH, 'user_list_principal_property_search')
-    _POST_AVAILABILITY = loadRequestBody(_LOAD_PATH, 'post_availability')
-
     @inlineCallbacks
-    def startup(self):
+    def postAttachment(self, href, content):
+        url = "{0}?{1}".format(href, "action=attachment-add")
+        filename = 'file-{}.txt'.format(len(content))
+        headers = Headers({
+        #     'Transfer-Encoding': ['Chunked'],
+            'Content-Disposition': ['attachment; filename="{}"'.format(filename)]
+        })
+        l = len(content)
+        # lengthPrefix = hex(l)[2:].upper() # For some reason, this attachment is length-prefixed in hex
+        label_suffix = self._getEventSizeDescription(l / 1024)
+        # body = "{0}\n{1}\n0\n".format(lengthPrefix, content) # XXX There HAS to be a better way to do this
+        yield self.requester.post(
+            url,
+            content,
+            headers=headers,
+            method_label="POST{attach-%s}" % (label_suffix,)
+        )
 
-        # Try to read data from disk - if it succeeds self.principalURL will be set
-        self.deserialize()
-
-        if self.principalURL is None:
-            # PROPFIND principal path to retrieve actual principal-URL
-            response = yield self._principalPropfindInitial(self.record.uid)
-            hrefs = response.getHrefProperties()
-            self.principalURL = hrefs[davxml.principal_URL].toString()
-
-        # Using the actual principal URL, retrieve principal information
-        principal = (yield self._extractPrincipalDetails())
-        returnValue(principal)
-
-
-
-class OS_X_10_7(BaseAppleClient):
-    """
-    Implementation of the OS X 10.7 iCal network behavior.
-    """
-
-    _client_type = "OS X 10.7"
-
-    USER_AGENT = "CalendarStore/5.0.2 (1166); iCal/5.0.2 (1571); Mac OS X/10.7.3 (11D50)"
-
-    # The default interval, used if none is specified in external
-    # configuration.  This is also the actual value used by Snow
-    # Leopard iCal.
-    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
-
-    # The maximum number of resources to retrieve in a single multiget
-    MULTIGET_BATCH_SIZE = 50
-
-    # Override and turn on if client supports Sync REPORT
-    _SYNC_REPORT = True
-
-    # Override and turn on if client syncs using time-range queries
-    _SYNC_TIMERANGE = False
-
-    # Override and turn off if client does not support attendee lookups
-    _ATTENDEE_LOOKUPS = True
-
-    # Request body data
-    _LOAD_PATH = "OS_X_10_7"
-
-    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
-    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
-    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
-    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
-    _STARTUP_PRINCIPAL_EXPAND = loadRequestBody(_LOAD_PATH, 'startup_principal_expand')
-    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
-    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
-    _STARTUP_PROPPATCH_CALENDAR_TIMEZONE = loadRequestBody(_LOAD_PATH, 'startup_calendar_timezone_proppatch')
-
-    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
-    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
-    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
-    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
-    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
-    _POLL_CALENDAR_SYNC_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_sync')
-    _POLL_NOTIFICATION_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
-    _POLL_NOTIFICATION_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_notification_propfind_d1')
-
-    _USER_LIST_PRINCIPAL_PROPERTY_SEARCH = loadRequestBody(_LOAD_PATH, 'user_list_principal_property_search')
-    _POST_AVAILABILITY = loadRequestBody(_LOAD_PATH, 'post_availability')
-
-
-    def _addDefaultHeaders(self, headers):
-        """
-        Add the clients default set of headers to ones being used in a request.
-        Default is to add User-Agent, sub-classes should override to add other
-        client specific things, Accept etc.
-        """
-
-        super(OS_X_10_7, self)._addDefaultHeaders(headers)
-        headers.setRawHeaders('Accept', ['*/*'])
-        headers.setRawHeaders('Accept-Language', ['en-us'])
-        headers.setRawHeaders('Accept-Encoding', ['gzip,deflate'])
-        headers.setRawHeaders('Connection', ['keep-alive'])
-
-
     @inlineCallbacks
-    def startup(self):
-
-        # Try to read data from disk - if it succeeds self.principalURL will be set
-        self.deserialize()
-
-        if self.principalURL is None:
-            # PROPFIND well-known with redirect
-            response = yield self._startupPropfindWellKnown()
-            hrefs = response.getHrefProperties()
-            if davxml.current_user_principal in hrefs:
-                self.principalURL = hrefs[davxml.current_user_principal].toString()
-            elif davxml.principal_URL in hrefs:
-                self.principalURL = hrefs[davxml.principal_URL].toString()
-            else:
-                # PROPFIND principal path to retrieve actual principal-URL
-                response = yield self._principalPropfindInitial(self.record.uid)
-                hrefs = response.getHrefProperties()
-                self.principalURL = hrefs[davxml.principal_URL].toString()
-
-        # Using the actual principal URL, retrieve principal information
-        principal = yield self._extractPrincipalDetails()
-        returnValue(principal)
-
-
-
-class iOS_5(BaseAppleClient):
-    """
-    Implementation of the iOS 5 network behavior.
-    """
-
-    _client_type = "iOS 5"
-
-    USER_AGENT = "iOS/5.1 (9B179) dataaccessd/1.0"
-
-    # The default interval, used if none is specified in external
-    # configuration.  This is also the actual value used by Snow
-    # Leopard iCal.
-    CALENDAR_HOME_POLL_INTERVAL = 15 * 60
-
-    # The maximum number of resources to retrieve in a single multiget
-    MULTIGET_BATCH_SIZE = 50
-
-    # Override and turn on if client supports Sync REPORT
-    _SYNC_REPORT = False
-
-    # Override and turn on if client syncs using time-range queries
-    _SYNC_TIMERANGE = True
-
-    # Override and turn off if client does not support attendee lookups
-    _ATTENDEE_LOOKUPS = False
-
-    # Request body data
-    _LOAD_PATH = "iOS_5"
-
-    _STARTUP_WELL_KNOWN = loadRequestBody(_LOAD_PATH, 'startup_well_known')
-    _STARTUP_PRINCIPAL_PROPFIND_INITIAL = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind_initial')
-    _STARTUP_PRINCIPAL_PROPFIND = loadRequestBody(_LOAD_PATH, 'startup_principal_propfind')
-    _STARTUP_PRINCIPALS_REPORT = loadRequestBody(_LOAD_PATH, 'startup_principals_report')
-    _STARTUP_PROPPATCH_CALENDAR_COLOR = loadRequestBody(_LOAD_PATH, 'startup_calendar_color_proppatch')
-    _STARTUP_PROPPATCH_CALENDAR_ORDER = loadRequestBody(_LOAD_PATH, 'startup_calendar_order_proppatch')
-
-    _POLL_CALENDARHOME_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendarhome_propfind')
-    _POLL_CALENDAR_PROPFIND = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind')
-    _POLL_CALENDAR_VEVENT_TR_QUERY = loadRequestBody(_LOAD_PATH, 'poll_calendar_vevent_tr_query')
-    _POLL_CALENDAR_VTODO_QUERY = loadRequestBody(_LOAD_PATH, 'poll_calendar_vtodo_query')
-    _POLL_CALENDAR_PROPFIND_D1 = loadRequestBody(_LOAD_PATH, 'poll_calendar_propfind_d1')
-    _POLL_CALENDAR_MULTIGET_REPORT = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget')
-    _POLL_CALENDAR_MULTIGET_REPORT_HREF = loadRequestBody(_LOAD_PATH, 'poll_calendar_multiget_hrefs')
-
-
-    def _addDefaultHeaders(self, headers):
+    def addCalendar(self, href, calendar_xml):
         """
-        Add the clients default set of headers to ones being used in a request.
-        Default is to add User-Agent, sub-classes should override to add other
-        client specific things, Accept etc.
+        client.addCalendar(
+            '/calendars/__uids__/10000000-0000-0000-0000-000000000001/1C1A8475-2671-4B97-AC58-DD9777B5CD93/',
+            # <Component: 'BEGIN:VCALENDAR\r\n...END:VCALENDAR\r\n'>)
+        )
         """
+        response = yield self.requester.mkcalendar(
+            href,
+            calendar_xml,
+            method_label="MK{calendar}",
+        )
+        # self._cacheCalendar(href, calendar)
 
-        super(iOS_5, self)._addDefaultHeaders(headers)
-        headers.setRawHeaders('Accept', ['*/*'])
-        headers.setRawHeaders('Accept-Language', ['en-us'])
-        headers.setRawHeaders('Accept-Encoding', ['gzip,deflate'])
-        headers.setRawHeaders('Connection', ['keep-alive'])
 
-
     @inlineCallbacks
-    def _pollFirstTime1(self, homeNode, calendars):
-        # Patch calendar properties
-        for cal in calendars:
-            if cal.name != "inbox":
-                yield self._proppatch(
-                    cal.url,
-                    self._STARTUP_PROPPATCH_CALENDAR_COLOR,
-                    method_label="PROPPATCH{calendar}",
-                )
-                yield self._proppatch(
-                    cal.url,
-                    self._STARTUP_PROPPATCH_CALENDAR_ORDER,
-                    method_label="PROPPATCH{calendar}",
-                )
+    def changeCalendar(self, href, body):
 
+        calendar = self._calendars[href]
+        headers = Headers({
+            'content-type': ['text/xml']
+        })
 
-    def _pollFirstTime2(self):
-        # Nothing here
-        return succeed(None)
+        # At last, upload the new event definition
+        response = yield self.requester.proppatch(
+            href,
+            body,
+            headers=headers,
+            method_label="PATCH{calendar}"
+        )
 
+        # Finally, re-retrieve the event to update the etag
+        # yield self._updateEvent(response, href)
 
-    def _updateCalendar(self, calendar, newToken):
-        """
-        Update the local cached data for a calendar in an appropriate manner.
-        """
-        if calendar.name == "inbox":
-            # Inbox is done as a PROPFIND Depth:1
-            return self._updateCalendar_PROPFIND(calendar, newToken)
-        elif "VEVENT" in calendar.componentTypes:
-            # VEVENTs done as time-range VEVENT-only queries
-            return self._updateCalendar_VEVENT(calendar, newToken)
-        elif "VTODO" in calendar.componentTypes:
-            # VTODOs done as VTODO-only queries
-            return self._updateCalendar_VTODO(calendar, newToken)
-
-
     @inlineCallbacks
-    def _updateCalendar_VEVENT(self, calendar, newToken):
-        """
-        Sync all locally cached VEVENTs using a VEVENT-only time-range query.
-        """
-
-        # Grab old hrefs prior to the PROPFIND so we sync with the old state. We need this because
-        # the sim can fire a PUT between the PROPFIND and when process the removals.
-        old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
-
-        now = DateTime.getNowUTC()
-        now.setDateOnly(True)
-        now.offsetMonth(-1) # 1 month back default
-        result = yield self._report(
-            calendar.url,
-            self._POLL_CALENDAR_VEVENT_TR_QUERY % {"start-date": now.getText()},
-            depth='1',
-            method_label="REPORT{vevent}",
+    def postXML(self, href, xml):
+        headers = Headers({
+            'content-type': ['text/xml']
+        })
+        response = yield self.requester.post(
+            href,
+            xml,
+            headers=headers,
+            method_label="SHARE{calendar}"
         )
 
-        yield self._updateApplyChanges(calendar, result, old_hrefs)
 
-        # Now update calendar to the new token
-        self._calendars[calendar.url].changeToken = newToken
-
-
     @inlineCallbacks
-    def _updateCalendar_VTODO(self, calendar, newToken):
+    def deleteCalendar(self, href):
         """
-        Sync all locally cached VTODOs using a VTODO-only query.
+        Issue a DELETE for the given URL and remove local state
+        associated with that calendar.
+
+        Usage: client.deleteCalendar('/calendars/__uids__/<user-uid>/<calendar-uid>/')
         """
 
-        # Grab old hrefs prior to the PROPFIND so we sync with the old state. We need this because
-        # the sim can fire a PUT between the PROPFIND and when process the removals.
-        old_hrefs = set([calendar.url + child for child in calendar.events.keys()])
+        self._invalidateCalendar(href)
 
-        result = yield self._report(
-            calendar.url,
-            self._POLL_CALENDAR_VTODO_QUERY,
-            depth='1',
-            method_label="REPORT{vtodo}",
-        )
+        response = yield self.requester.delete(href, method_label="DELETE{calendar}")
+        returnValue(response)
 
-        yield self._updateApplyChanges(calendar, result, old_hrefs)
 
-        # Now update calendar to the new token
-        self._calendars[calendar.url].changeToken = newToken
 
-
-    @inlineCallbacks
-    def startup(self):
-
-        # Try to read data from disk - if it succeeds self.principalURL will be set
-        self.deserialize()
-
-        if self.principalURL is None:
-            # PROPFIND well-known with redirect
-            response = yield self._startupPropfindWellKnown()
-            hrefs = response.getHrefProperties()
-            if davxml.current_user_principal in hrefs:
-                self.principalURL = hrefs[davxml.current_user_principal].toString()
-            elif davxml.principal_URL in hrefs:
-                self.principalURL = hrefs[davxml.principal_URL].toString()
-            else:
-                # PROPFIND principal path to retrieve actual principal-URL
-                response = yield self._principalPropfindInitial(self.record.uid)
-                hrefs = response.getHrefProperties()
-                self.principalURL = hrefs[davxml.principal_URL].toString()
-
-        # Using the actual principal URL, retrieve principal information
-        principal = yield self._extractPrincipalDetails()
-        returnValue(principal)
-
-
-
-class RequestLogger(object):
-    format = u"%(user)s request %(code)s%(success)s[%(duration)5.2f s] %(method)8s %(url)s"
-    success = u"\N{CHECK MARK}"
-    failure = u"\N{BALLOT X}"
-
-    def observe(self, event):
-        if event.get("type") == "response":
-            formatArgs = dict(
-                user=event['user'],
-                method=event['method'],
-                url=urlunparse(('', '') + urlparse(event['url'])[2:]),
-                code=event['code'],
-                duration=event['duration'],
-            )
-
-            if event['success']:
-                formatArgs['success'] = self.success
-            else:
-                formatArgs['success'] = self.failure
-            print((self.format % formatArgs).encode('utf-8'))
-
-
-    def report(self, output):
-        pass
-
-
-    def failures(self):
-        return []
-
-
-
 def main():
     from urllib2 import HTTPDigestAuthHandler
     from twisted.internet import reactor
+    from twisted.python.log import addObserver
+    from contrib.performance.loadtest.logger import RequestLogger
+    from contrib.performance.loadtest.clients import OS_X_10_11
     auth = HTTPDigestAuthHandler()
     auth.add_password(
         realm="Test Realm",
@@ -2083,14 +1424,17 @@
 
     addObserver(RequestLogger().observe)
 
-    from sim import _DirectoryRecord
-    client = OS_X_10_6(
-        reactor, 'http://127.0.0.1:8008/',
-        _DirectoryRecord(
-            u'user01', u'user01', u'User 01', u'user01 at example.org'),
-        auth)
+    from contrib.performance.loadtest.records import DirectoryRecord
+    client = OS_X_10_11(
+        reactor,
+        'http://127.0.0.1:8008/',   # root
+        '/tmp/sim',                 # serializePath
+        DirectoryRecord(u'user01', u'user01', u'User 01', u'user01 at example.org', u'10000000-0000-0000-0000-000000000001'),
+        auth,
+        title='OS X 10.11 Client Simulator'
+    )
     d = client.run()
-    d.addErrback(err, "10.6 client run() problem")
+    d.addErrback(err, "10.11 client run() problem")
     d.addCallback(lambda ignored: reactor.stop())
     reactor.run()
 

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/logger.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/logger.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/logger.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -14,9 +14,27 @@
 # limitations under the License.
 #
 ##
+import json
+import collections
+import os
+import sys
+from datetime import datetime
 
-from contrib.performance.stats import mean, median, stddev
+from urlparse import urlparse, urlunparse
 
+from contrib.performance.stats import mean, median, stddev, mad
+
+class TerminalColors:
+    HEADER = '\033[95m'
+    OKBLUE = '\033[94m'
+    OKGREEN = '\033[92m'
+    WARNING = '\033[93m'
+    LIGHTBLUE = '\033[36m'
+    FAIL = '\033[91m'
+    ENDC = '\033[0m'
+    BOLD = '\033[1m'
+    UNDERLINE = '\033[4m'
+
 class SummarizingMixin(object):
 
     def printHeader(self, output, fields):
@@ -87,3 +105,496 @@
         """
         for method, data in perOperationTimes:
             self._printRow(output, formats, self._summarizeData(method, data))
+
+
+class MessageLogger(object):
+    def observe(self, event):
+        if event.get("type") == "log":
+            import random
+            identifier = random.random()
+            print(TerminalColors.WARNING + str(identifier) + '/' + event.get('val') + ':' + event.get('text') + TerminalColors.ENDC)
+
+    def report(self, output):
+        pass
+
+
+    def failures(self):
+        return []
+
+
+class EverythingLogger(object):
+    def observe(self, event):
+        # if event.get("type") == "response":
+        #     from pprint import pprint
+        #     pprint(event)
+        pass
+
+    def report(self, output):
+        pass
+
+
+    def failures(self):
+        return []
+
+
+class RequestLogger(object):
+    format = u"%(user)s request %(code)s%(success)s[%(duration)5.2f s] %(method)8s %(url)s"
+    success = u"\N{CHECK MARK}"
+    failure = u"\N{BALLOT X}"
+
+    def observe(self, event):
+        if event.get("type") == "response":
+            formatArgs = dict(
+                user=event['user'],
+                method=event['method'],
+                url=urlunparse(('', '') + urlparse(event['url'])[2:]),
+                code=event['code'],
+                duration=event['duration'],
+            )
+
+            if event['success']:
+                formatArgs['success'] = self.success
+                start = TerminalColors.OKGREEN
+            else:
+                formatArgs['success'] = self.failure
+                start = TerminalColors.FAIL
+            print(start + (self.format % formatArgs).encode('utf-8') + "from Logger w/ id: " + str(id(self)) + TerminalColors.ENDC)
+
+
+    def report(self, output):
+        pass
+
+
+    def failures(self):
+        return []
+
+
+
+class OperationLogger(SummarizingMixin):
+    """
+    Profiles will initiate operations which may span multiple requests.  Start
+    and stop log messages are emitted for these operations and logged by this
+    logger.
+    """
+    formats = {
+        u"start" : u"%(user)s - - - - - - - - - - - %(label)8s BEGIN %(lag)s",
+        u"end"   : u"%(user)s - - - - - - - - - - - %(label)8s END [%(duration)5.2f s]",
+        u"failed": u"%(user)s x x x x x x x x x x x %(label)8s FAILED %(reason)s",
+    }
+
+    lagFormat = u'{lag %5.2f ms}'
+
+    # the response time thresholds to display together with failing % count threshold
+    _thresholds_default = {
+        "operations": {
+            "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
+            "thresholds": {
+                "default": [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0],
+            }
+        }
+    }
+    _lag_cut_off = 1.0      # Maximum allowed median scheduling latency, seconds
+    _fail_cut_off = 1.0     # % of total count at which failed requests will cause a failure
+
+    _fields_init = [
+        ('operation', -25, '%-25s'),
+        ('count', 8, '%8s'),
+        ('failed', 8, '%8s'),
+    ]
+
+    _fields_extend = [
+        ('mean', 8, '%8.4f'),
+        ('median', 8, '%8.4f'),
+        ('stddev', 8, '%8.4f'),
+        ('avglag (ms)', 12, '%12.4f'),
+        ('STATUS', 8, '%8s'),
+    ]
+
+    def __init__(self, outfile=None, **params):
+        self._perOperationTimes = {}
+        self._perOperationLags = {}
+        if outfile is None:
+            outfile = sys.stdout
+        self._outfile = outfile
+
+        # Load parameters from config
+        if "thresholdsPath" in params:
+            jsondata = json.load(open(params["thresholdsPath"]))
+        elif "thresholds" in params:
+            jsondata = params["thresholds"]
+        else:
+            jsondata = self._thresholds_default
+        self._thresholds = [[limit, {}] for limit in jsondata["operations"]["limits"]]
+        for ctr, item in enumerate(self._thresholds):
+            for k, v in jsondata["operations"]["thresholds"].items():
+                item[1][k] = v[ctr]
+
+        self._fields = self._fields_init[:]
+        for threshold, _ignore_fail_at in self._thresholds:
+            self._fields.append(('>%g sec' % (threshold,), 10, '%10s'))
+        self._fields.extend(self._fields_extend)
+
+        if "lagCutoff" in params:
+            self._lag_cut_off = params["lagCutoff"]
+
+        if "failCutoff" in params:
+            self._fail_cut_off = params["failCutoff"]
+
+
+    def observe(self, event):
+        if event.get("type") == "operation":
+            event = event.copy()
+            lag = event.get('lag')
+            if lag is None:
+                event['lag'] = ''
+            else:
+                event['lag'] = self.lagFormat % (lag * 1000.0,)
+
+            self._outfile.write(
+                TerminalColors.LIGHTBLUE +
+                (self.formats[event[u'phase']] % event).encode('utf-8') + TerminalColors.ENDC + '\n')
+
+            if event[u'phase'] == u'end':
+                dataset = self._perOperationTimes.setdefault(event[u'label'], [])
+                dataset.append((event[u'success'], event[u'duration']))
+            elif lag is not None:
+                dataset = self._perOperationLags.setdefault(event[u'label'], [])
+                dataset.append(lag)
+
+
+    def _summarizeData(self, operation, data):
+        avglag = mean(self._perOperationLags.get(operation, [0.0])) * 1000.0
+        data = SummarizingMixin._summarizeData(self, operation, data)
+        return data[:-1] + (avglag,) + data[-1:]
+
+
+    def report(self, output):
+        output.write("\n")
+        self.printHeader(output, [
+            (label, width)
+            for (label, width, _ignore_fmt) in self._fields
+        ])
+        self.printData(
+            output,
+            [fmt for (label, width, fmt) in self._fields],
+            sorted(self._perOperationTimes.items())
+        )
+
+    _LATENCY_REASON = "Median %(operation)s scheduling lag greater than %(cutoff)sms"
+    _FAILED_REASON = "Greater than %(cutoff).0f%% %(operation)s failed"
+
+    def failures(self):
+        reasons = []
+
+        for operation, lags in self._perOperationLags.iteritems():
+            if median(lags) > self._lag_cut_off:
+                reasons.append(self._LATENCY_REASON % dict(
+                    operation=operation.upper(), cutoff=self._lag_cut_off * 1000))
+
+        for operation, times in self._perOperationTimes.iteritems():
+            failures = len([success for (success, _ignore_duration) in times if not success])
+            if failures * 100.0 / len(times) > self._fail_cut_off:
+                reasons.append(self._FAILED_REASON % dict(
+                    operation=operation.upper(), cutoff=self._fail_cut_off))
+
+        return reasons
+
+
+
+class StatisticsBase(object):
+    def observe(self, event):
+        if event.get('type') == 'response':
+            self.eventReceived(event)
+        elif event.get('type') == 'client-failure':
+            self.clientFailure(event)
+        elif event.get('type') == 'sim-failure':
+            self.simFailure(event)
+
+
+    def report(self, output):
+        pass
+
+
+    def failures(self):
+        return []
+
+
+
+class SimpleStatistics(StatisticsBase):
+    def __init__(self):
+        self._times = []
+        self._failures = collections.defaultdict(int)
+        self._simFailures = collections.defaultdict(int)
+
+
+    def eventReceived(self, event):
+        self._times.append(event['duration'])
+        if len(self._times) == 200:
+            print('mean:', mean(self._times))
+            print('median:', median(self._times))
+            print('stddev:', stddev(self._times))
+            print('mad:', mad(self._times))
+            del self._times[:100]
+
+
+    def clientFailure(self, event):
+        self._failures[event] += 1
+
+
+    def simFailure(self, event):
+        self._simFailures[event] += 1
+
+
+
+class ReportStatistics(StatisticsBase, SummarizingMixin):
+    """
+
+    @ivar _users: A C{set} containing all user UIDs which have been observed in
+        events.  When generating the final report, the size of this set is
+        reported as the number of users in the simulation.
+
+    """
+
+    # the response time thresholds to display together with failing % count threshold
+    _thresholds_default = {
+        "requests": {
+            "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
+            "thresholds": {
+                "default": [100.0, 100.0, 100.0, 5.0, 1.0, 0.5, 0.0],
+            }
+        }
+    }
+    _fail_cut_off = 1.0     # % of total count at which failed requests will cause a failure
+
+    _fields_init = [
+        ('request', -25, '%-25s'),
+        ('count', 8, '%8s'),
+        ('failed', 8, '%8s'),
+    ]
+
+    _fields_extend = [
+        ('mean', 8, '%8.4f'),
+        ('median', 8, '%8.4f'),
+        ('stddev', 8, '%8.4f'),
+        ('QoS', 8, '%8.4f'),
+        ('STATUS', 8, '%8s'),
+    ]
+
+    def __init__(self, **params):
+        self._perMethodTimes = {}
+        self._users = set()
+        self._clients = set()
+        self._failed_clients = []
+        self._failed_sim = collections.defaultdict(int)
+        self._startTime = datetime.now()
+        self._expired_data = None
+
+        # Load parameters from config
+        if "thresholdsPath" in params:
+            jsondata = json.load(open(params["thresholdsPath"]))
+        elif "thresholds" in params:
+            jsondata = params["thresholds"]
+        else:
+            jsondata = self._thresholds_default
+        self._thresholds = [[limit, {}] for limit in jsondata["requests"]["limits"]]
+        for ctr, item in enumerate(self._thresholds):
+            for k, v in jsondata["requests"]["thresholds"].items():
+                item[1][k] = v[ctr]
+
+        self._fields = self._fields_init[:]
+        for threshold, _ignore_fail_at in self._thresholds:
+            self._fields.append(('>%g sec' % (threshold,), 10, '%10s'))
+        self._fields.extend(self._fields_extend)
+
+        if "benchmarksPath" in params:
+            self.benchmarks = json.load(open(params["benchmarksPath"]))
+        else:
+            self.benchmarks = {}
+
+        if "failCutoff" in params:
+            self._fail_cut_off = params["failCutoff"]
+
+
+    def observe(self, event):
+        if event.get('type') == 'sim-expired':
+            self.simExpired(event)
+        else:
+            super(ReportStatistics, self).observe(event)
+
+
+    def countUsers(self):
+        return len(self._users)
+
+
+    def countClients(self):
+        return len(self._clients)
+
+
+    def countClientFailures(self):
+        return len(self._failed_clients)
+
+
+    def countSimFailures(self):
+        return len(self._failed_sim)
+
+
+    def eventReceived(self, event):
+        dataset = self._perMethodTimes.setdefault(event['method'], [])
+        dataset.append((event['success'], event['duration']))
+        self._users.add(event['user'])
+        self._clients.add(event['client_id'])
+
+
+    def clientFailure(self, event):
+        self._failed_clients.append(event['reason'])
+
+
+    def simFailure(self, event):
+        self._failed_sim[event['reason']] += 1
+
+
+    def simExpired(self, event):
+        self._expired_data = event['reason']
+
+
+    def printMiscellaneous(self, output, items):
+        maxColumnWidth = str(len(max(items.iterkeys(), key=len)))
+        fmt = "%" + maxColumnWidth + "s : %-s\n"
+        for k in sorted(items.iterkeys()):
+            output.write(fmt % (k.title(), items[k],))
+
+
+    def qos(self):
+        """
+        Determine a "quality of service" value that can be used for comparisons between runs. This value
+        is based on the percentage deviation of means of each request from a set of "benchmarks" for each
+        type of request.
+        """
+
+        # Get means for each type of method
+        means = {}
+        for method, results in self._perMethodTimes.items():
+            means[method] = mean([duration for success, duration in results if success])
+
+        # Determine percentage differences with weighting
+        differences = []
+        for method, value in means.items():
+            result = self.qos_value(method, value)
+            if result is not None:
+                differences.append(result)
+
+        return ("%-8.4f" % mean(differences)) if differences else "None"
+
+
+    def qos_value(self, method, value):
+        benchmark = self.benchmarks.get(method)
+        if benchmark is None:
+            return None
+        test_mean, weight = (benchmark["mean"], benchmark["weight"],)
+        return ((value / test_mean) - 1.0) * weight + 1.0
+
+
+    def _summarizeData(self, operation, data):
+        data = SummarizingMixin._summarizeData(self, operation, data)
+        value = self.qos_value(operation, data[-4])
+        if value is None:
+            value = 0.0
+        return data[:-1] + (value,) + data[-1:]
+
+
+    def report(self, output):
+        output.write("\n")
+        output.write("** REPORT **\n")
+        output.write("\n")
+        runtime = datetime.now() - self._startTime
+        cpu = os.times()
+        cpuUser = cpu[0] + cpu[2]
+        cpuSys = cpu[1] + cpu[3]
+        cpuTotal = cpuUser + cpuSys
+        runHours, remainder = divmod(runtime.seconds, 3600)
+        runMinutes, runSeconds = divmod(remainder, 60)
+        cpuHours, remainder = divmod(cpuTotal, 3600)
+        cpuMinutes, cpuSeconds = divmod(remainder, 60)
+        items = {
+            'Users': self.countUsers(),
+            'Clients': self.countClients(),
+            'Start time': self._startTime.strftime('%m/%d %H:%M:%S'),
+            'Run time': "%02d:%02d:%02d" % (runHours, runMinutes, runSeconds),
+            'CPU Time': "user %-5.2f sys %-5.2f total %02d:%02d:%02d" % (cpuUser, cpuSys, cpuHours, cpuMinutes, cpuSeconds,),
+            'QoS': self.qos(),
+        }
+        if self.countClientFailures() > 0:
+            items['Failed clients'] = self.countClientFailures()
+            for ctr, reason in enumerate(self._failed_clients, 1):
+                items['Failure #%d' % (ctr,)] = reason
+        if self.countSimFailures() > 0:
+            for reason, count in self._failed_sim.items():
+                items['Failed operation'] = "%s : %d times" % (reason, count,)
+        output.write("* Client\n")
+        self.printMiscellaneous(output, items)
+        output.write("\n")
+
+        if self._expired_data is not None:
+            items = {
+                "Req/sec" : "%.1f" % (self._expired_data[0],),
+                "Response": "%.1f (ms)" % (self._expired_data[1],),
+                "Slots": "%.2f" % (self._expired_data[2],),
+                "CPU": "%.1f%%" % (self._expired_data[3],),
+            }
+            output.write("* Server (Last 5 minutes)\n")
+            self.printMiscellaneous(output, items)
+            output.write("\n")
+        output.write("* Details\n")
+
+        self.printHeader(output, [
+            (label, width)
+            for (label, width, _ignore_fmt)
+            in self._fields
+        ])
+        self.printData(
+            output,
+            [fmt for (label, width, fmt) in self._fields],
+            sorted(self._perMethodTimes.items())
+        )
+
+    _FAILED_REASON = "Greater than %(cutoff)g%% %(method)s failed"
+
+    _REASON_1 = "Greater than %(cutoff)g%% %(method)s exceeded "
+    _REASON_2 = "%g second response time"
+
+    def failures(self):
+        # TODO
+        reasons = []
+
+        for (method, times) in self._perMethodTimes.iteritems():
+            failures = 0
+            overDurations = [0] * len(self._thresholds)
+
+            for success, duration in times:
+                if not success:
+                    failures += 1
+                for ctr, item in enumerate(self._thresholds):
+                    threshold, _ignore_fail_at = item
+                    if duration > threshold:
+                        overDurations[ctr] += 1
+
+            checks = [
+                (failures, self._fail_cut_off, self._FAILED_REASON),
+            ]
+
+            for ctr, item in enumerate(self._thresholds):
+                threshold, fail_at = item
+                fail_at = fail_at.get(method, fail_at["default"])
+                checks.append(
+                    (overDurations[ctr], fail_at, self._REASON_1 + self._REASON_2 % (threshold,))
+                )
+
+            for count, cutoff, reason in checks:
+                if count * 100.0 / len(times) > cutoff:
+                    reasons.append(reason % dict(method=method, cutoff=cutoff))
+
+        if self.countClientFailures() != 0:
+            reasons.append("Client failures: %d" % (self.countClientFailures(),))
+        if self.countSimFailures() != 0:
+            reasons.append("Overall failures: %d" % (self.countSimFailures(),))
+        return reasons

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/population.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/population.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/population.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -25,13 +25,10 @@
 
 from tempfile import mkdtemp
 from itertools import izip
-from datetime import datetime
+
 from urllib2 import HTTPBasicAuthHandler
 from urllib2 import HTTPDigestAuthHandler
 from urllib2 import HTTPPasswordMgrWithDefaultRealm
-import collections
-import json
-import os
 
 from twisted.internet.defer import DeferredList
 from twisted.python.failure import Failure
@@ -41,54 +38,59 @@
 
 from twistedcaldav.timezones import TimezoneCache
 
-from contrib.performance.stats import mean, median, stddev, mad
 from contrib.performance.loadtest.trafficlogger import loggedReactor
-from contrib.performance.loadtest.logger import SummarizingMixin
-from contrib.performance.loadtest.ical import OS_X_10_6, RequestLogger
+
 from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
 
 
-class ProfileType(object, FancyEqMixin):
-    """
-    @ivar profileType: A L{ProfileBase} subclass, or an L{ICalendarUserProfile}
-        implementation.
+# class ProfileType(object, FancyEqMixin):
+#     """
+#     @ivar profileType: A L{ProfileBase} subclass
+#     @type profileType: C{type}
 
-    @ivar params: A C{dict} which will be passed to C{profileType} as keyword
-        arguments to create a new profile instance.
-    """
-    compareAttributes = ("profileType", "params")
+#     @ivar params: A C{dict} which will be passed to C{profileType} as keyword
+#         arguments to create a new profile instance.
+#     """
+#     compareAttributes = ("profileType", "params")
 
-    def __init__(self, profileType, params):
-        self.profileType = profileType
-        self.params = params
+#     def __init__(self, profileType, params):
+#         self.profileType = profileType
+#         self.params = params
 
 
-    def __call__(self, reactor, simulator, client, number):
-        return self.profileType(reactor, simulator, client, number, **self.params)
+#     def __call__(self, reactor, simulator, client, number):
+#         base = self.profileType(**self.params)
+#         base.setUp(reactor, simulator, client, number)
+#         return base
 
 
+#     def __repr__(self):
+#         return "ProfileType(%s, params=%s)" % (self.profileType.__name__, self.params)
 
-class ClientType(object, FancyEqMixin):
+
+
+class ClientFactory(object, FancyEqMixin):
     """
-    @ivar clientType: An L{ICalendarClient} implementation
+    @ivar clientType: An L{BaseAppleClient} subclass
+    @ivar params: A C{dict} which will be passed to C{clientType} as keyword
+        arguements to create a new client
     @ivar profileTypes: A list of L{ProfileType} instances
     """
     compareAttributes = ("clientType", "profileTypes")
 
-    def __init__(self, clientType, clientParams, profileTypes):
+    def __init__(self, clientType, clientParams, profiles):
         self.clientType = clientType
         self.clientParams = clientParams
-        self.profileTypes = profileTypes
+        self.profiles = profiles
 
 
-    def new(self, reactor, serverAddress, principalPathTemplate, serializationPath, userRecord, authInfo):
+    def new(self, reactor, serverAddress, serializationPath, userRecord, authInfo):
         """
         Create a new instance of this client type.
         """
         return self.clientType(
-            reactor, serverAddress, principalPathTemplate,
-            serializationPath, userRecord, authInfo,
-            **self.clientParams
+            reactor, serverAddress, serializationPath,
+            userRecord, authInfo, **self.clientParams
         )
 
 
@@ -118,66 +120,74 @@
         self.clients.append((weight, clientType))
 
 
-    def clientTypes(self):
-        """
-        Return a list of two-tuples giving the weights and types of
-        clients in the population.
-        """
-        return self.clients
+    def clientGenerator(self):
+        while True:
+            for (weight, clientFactory) in self.clients:
+                for _ignore_i in xrange(weight):
+                    yield clientFactory
 
 
+    # def clientTypes(self):
+    #     """
+    #     Return a list of two-tuples giving the weights and types of
+    #     clients in the population.
+    #     """
+    #     return self.clients
 
-class Populator(object):
-    """
-    @ivar userPattern: A C{str} giving a formatting pattern to use to
-        construct usernames.  The string will be interpolated with a
-        single integer, the incrementing counter of how many users
-        have thus far been "used".
 
-    @ivar passwordPattern: Similar to C{userPattern}, but for
-        passwords.
-    """
-    def __init__(self, random):
-        self._random = random
 
 
-    def _cycle(self, elements):
-        while True:
-            for (weight, value) in elements:
-                for _ignore_i in range(weight):
-                    yield value
 
+# class Populator(object):
+#     """
+#     """
+#     def __init__(self):
+#         self._random = random
 
-    def populate(self, parameters):
-        """
-        Generate individuals such as might be randomly selected from a
-        population with the given parameters.
 
-        @type parameters: L{PopulationParameters}
-        @rtype: generator of L{ClientType} instances
-        """
-        for (clientType,) in izip(self._cycle(parameters.clientTypes())):
-            yield clientType
+#     def _cycle(self, elements):
+#         while True:
+#             for (weight, value) in elements:
+#                 for _ignore_i in range(weight):
+#                     yield value
 
 
+#     def populate(self, parameters):
+#         """
+#         Generate individuals such as might be randomly selected from a
+#         population with the given parameters.
 
+#         @type parameters: L{PopulationParameters}
+#         @rtype: generator of L{ClientType} instances
+#         """
+#         for (clientType,) in izip(self._cycle(parameters.clientTypes())):
+#             yield clientType
+
+
+
 class CalendarClientSimulator(object):
-    def __init__(self, records, populator, parameters, reactor, server,
-                 principalPathTemplate, serializationPath, workerIndex=0, workerCount=1):
+    def __init__(self, records, parameters, reactor, server,
+                 serializationPath, workerIndex=0, workerCount=1):
+        import random
+        # i = random.randint(0, 1000)
+        # with open('log%d.txt'.format(i), 'a') as f:
+        #     f.write('wtf')
+        val = random.random()
+        msg(type="log", text="In create client sim", val=str(val))
+        # from pprint import pprint
+        # pprint(records)
         self._records = records
-        self.populator = populator
         self.reactor = reactor
         self.server = server
-        self.principalPathTemplate = principalPathTemplate
         self.serializationPath = serializationPath
-        self._pop = self.populator.populate(parameters)
+        self._populator = parameters.clientGenerator()
         self._user = 0
         self._stopped = False
         self.workerIndex = workerIndex
         self.workerCount = workerCount
         self.clients = []
 
-        TimezoneCache.create()
+        # TimezoneCache.create()
 
 
     def getUserRecord(self, index):
@@ -225,47 +235,71 @@
 
 
     def add(self, numClients, clientsPerUser):
-        for _ignore_n in range(numClients):
-            number = self._nextUserNumber()
+        # for _ignore_n in range(numClients):
+        #     number = self._nextUserNumber()
 
-            for _ignore_peruser in range(clientsPerUser):
-                clientType = self._pop.next()
+        #     for _ignore_peruser in range(clientsPerUser):
+        #         clientType = self._populator.next()
+        #         if (number % self.workerCount) != self.workerIndex:
+        #             # If we're in a distributed work scenario and we are worker N,
+        #             # we have to skip all but every Nth request (since every node
+        #             # runs the same arrival policy).
+        #             continue
+
+        #         _ignore_user, auth = self._createUser(number)
+
+        #         reactor = loggedReactor(self.reactor)
+        #         client = clientType.new(
+        #             reactor,
+        #             self.server,
+        #             self.serializationPath,
+        #             self.getUserRecord(number),
+        #             auth,
+        #         )
+        #         self.clients.append(client)
+        #         d = client.run()
+        #         d.addErrback(self._clientFailure, reactor)
+
+        #         for profileType in clientType.profileTypes:
+        #             print(profileType)
+        #             profile = profileType(reactor, self, client, number)
+        #             if profile.enabled:
+        #                 d = profile.initialize()
+        #                 def _run(result):
+        #                     d2 = profile.run()
+        #                     d2.addErrback(self._profileFailure, profileType, reactor)
+        #                     return d2
+        #                 d.addCallback(_run)
+
+        for i in range(numClients):
+            number = self._nextUserNumber()
+            # What user are we representing?
+            for j in range(clientsPerUser):
                 if (number % self.workerCount) != self.workerIndex:
                     # If we're in a distributed work scenario and we are worker N,
                     # we have to skip all but every Nth request (since every node
                     # runs the same arrival policy).
                     continue
+                clientFactory = self._populator.next()
 
                 _ignore_user, auth = self._createUser(number)
-
                 reactor = loggedReactor(self.reactor)
-                client = clientType.new(
-                    reactor,
+
+                client = clientFactory.new(
+                    self.reactor,
                     self.server,
-                    self.principalPathTemplate,
                     self.serializationPath,
                     self.getUserRecord(number),
-                    auth,
+                    auth
                 )
                 self.clients.append(client)
-                d = client.run()
-                d.addErrback(self._clientFailure, reactor)
+                client.run().addErrback(self._clientFailure, reactor)
+                for profileTemplate in clientFactory.profiles:
+                    profile = profileTemplate.duplicate()
+                    profile.setUp(self.reactor, self, client, number)
+                    profile.run().addErrback(self._profileFailure, reactor)
 
-                for profileType in clientType.profileTypes:
-                    profile = profileType(reactor, self, client, number)
-                    if profile.enabled:
-                        d = profile.initialize()
-                        def _run(result):
-                            d2 = profile.run()
-                            d2.addErrback(self._profileFailure, profileType, reactor)
-                            return d2
-                        d.addCallback(_run)
 
-        # XXX this status message is prone to be slightly inaccurate, but isn't
-        # really used by much anyway.
-        msg(type="status", clientCount=self._user - 1)
-
-
     def _dumpLogs(self, loggingReactor, reason):
         path = FilePath(mkdtemp())
         logstate = loggingReactor.getLogFiles()
@@ -278,6 +312,13 @@
         return path
 
 
+    def _profileFailure(self, reason, reactor):
+        if not self._stopped:
+            where = self._dumpLogs(reactor, reason)
+            err(reason, "Profile stopped with error; recent traffic in %r" % (
+                where.path,))
+
+
     def _clientFailure(self, reason, reactor):
         if not self._stopped:
             where = self._dumpLogs(reactor, reason)
@@ -288,13 +329,6 @@
             msg(type="client-failure", reason="%s: %s" % (reason.type, reason.value,))
 
 
-    def _profileFailure(self, reason, profileType, reactor):
-        if not self._stopped:
-            where = self._dumpLogs(reactor, reason)
-            err(reason, "Profile stopped with error; recent traffic in %r" % (
-                where.path,))
-
-
     def _simFailure(self, reason, reactor):
         if not self._stopped:
             msg(type="sim-failure", reason=reason)
@@ -302,321 +336,20 @@
 
 
 class SmoothRampUp(object):
-    def __init__(self, reactor, groups, groupSize, interval, clientsPerUser):
-        self.reactor = reactor
+    def __init__(self, groups, groupSize, interval, clientsPerUser):
         self.groups = groups
         self.groupSize = groupSize
         self.interval = interval
         self.clientsPerUser = clientsPerUser
 
 
-    def run(self, simulator):
+    def run(self, reactor, simulator):
         for i in range(self.groups):
-            self.reactor.callLater(
+            reactor.callLater(
                 self.interval * i, simulator.add, self.groupSize, self.clientsPerUser)
 
 
 
-class StatisticsBase(object):
-    def observe(self, event):
-        if event.get('type') == 'response':
-            self.eventReceived(event)
-        elif event.get('type') == 'client-failure':
-            self.clientFailure(event)
-        elif event.get('type') == 'sim-failure':
-            self.simFailure(event)
-
-
-    def report(self, output):
-        pass
-
-
-    def failures(self):
-        return []
-
-
-
-class SimpleStatistics(StatisticsBase):
-    def __init__(self):
-        self._times = []
-        self._failures = collections.defaultdict(int)
-        self._simFailures = collections.defaultdict(int)
-
-
-    def eventReceived(self, event):
-        self._times.append(event['duration'])
-        if len(self._times) == 200:
-            print('mean:', mean(self._times))
-            print('median:', median(self._times))
-            print('stddev:', stddev(self._times))
-            print('mad:', mad(self._times))
-            del self._times[:100]
-
-
-    def clientFailure(self, event):
-        self._failures[event] += 1
-
-
-    def simFailure(self, event):
-        self._simFailures[event] += 1
-
-
-
-class ReportStatistics(StatisticsBase, SummarizingMixin):
-    """
-
-    @ivar _users: A C{set} containing all user UIDs which have been observed in
-        events.  When generating the final report, the size of this set is
-        reported as the number of users in the simulation.
-
-    """
-
-    # the response time thresholds to display together with failing % count threshold
-    _thresholds_default = {
-        "requests": {
-            "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
-            "thresholds": {
-                "default": [100.0, 100.0, 100.0, 5.0, 1.0, 0.5, 0.0],
-            }
-        }
-    }
-    _fail_cut_off = 1.0     # % of total count at which failed requests will cause a failure
-
-    _fields_init = [
-        ('request', -25, '%-25s'),
-        ('count', 8, '%8s'),
-        ('failed', 8, '%8s'),
-    ]
-
-    _fields_extend = [
-        ('mean', 8, '%8.4f'),
-        ('median', 8, '%8.4f'),
-        ('stddev', 8, '%8.4f'),
-        ('QoS', 8, '%8.4f'),
-        ('STATUS', 8, '%8s'),
-    ]
-
-    def __init__(self, **params):
-        self._perMethodTimes = {}
-        self._users = set()
-        self._clients = set()
-        self._failed_clients = []
-        self._failed_sim = collections.defaultdict(int)
-        self._startTime = datetime.now()
-        self._expired_data = None
-
-        # Load parameters from config
-        if "thresholdsPath" in params:
-            jsondata = json.load(open(params["thresholdsPath"]))
-        elif "thresholds" in params:
-            jsondata = params["thresholds"]
-        else:
-            jsondata = self._thresholds_default
-        self._thresholds = [[limit, {}] for limit in jsondata["requests"]["limits"]]
-        for ctr, item in enumerate(self._thresholds):
-            for k, v in jsondata["requests"]["thresholds"].items():
-                item[1][k] = v[ctr]
-
-        self._fields = self._fields_init[:]
-        for threshold, _ignore_fail_at in self._thresholds:
-            self._fields.append(('>%g sec' % (threshold,), 10, '%10s'))
-        self._fields.extend(self._fields_extend)
-
-        if "benchmarksPath" in params:
-            self.benchmarks = json.load(open(params["benchmarksPath"]))
-        else:
-            self.benchmarks = {}
-
-        if "failCutoff" in params:
-            self._fail_cut_off = params["failCutoff"]
-
-
-    def observe(self, event):
-        if event.get('type') == 'sim-expired':
-            self.simExpired(event)
-        else:
-            super(ReportStatistics, self).observe(event)
-
-
-    def countUsers(self):
-        return len(self._users)
-
-
-    def countClients(self):
-        return len(self._clients)
-
-
-    def countClientFailures(self):
-        return len(self._failed_clients)
-
-
-    def countSimFailures(self):
-        return len(self._failed_sim)
-
-
-    def eventReceived(self, event):
-        dataset = self._perMethodTimes.setdefault(event['method'], [])
-        dataset.append((event['success'], event['duration']))
-        self._users.add(event['user'])
-        self._clients.add(event['client_id'])
-
-
-    def clientFailure(self, event):
-        self._failed_clients.append(event['reason'])
-
-
-    def simFailure(self, event):
-        self._failed_sim[event['reason']] += 1
-
-
-    def simExpired(self, event):
-        self._expired_data = event['reason']
-
-
-    def printMiscellaneous(self, output, items):
-        maxColumnWidth = str(len(max(items.iterkeys(), key=len)))
-        fmt = "%" + maxColumnWidth + "s : %-s\n"
-        for k in sorted(items.iterkeys()):
-            output.write(fmt % (k.title(), items[k],))
-
-
-    def qos(self):
-        """
-        Determine a "quality of service" value that can be used for comparisons between runs. This value
-        is based on the percentage deviation of means of each request from a set of "benchmarks" for each
-        type of request.
-        """
-
-        # Get means for each type of method
-        means = {}
-        for method, results in self._perMethodTimes.items():
-            means[method] = mean([duration for success, duration in results if success])
-
-        # Determine percentage differences with weighting
-        differences = []
-        for method, value in means.items():
-            result = self.qos_value(method, value)
-            if result is not None:
-                differences.append(result)
-
-        return ("%-8.4f" % mean(differences)) if differences else "None"
-
-
-    def qos_value(self, method, value):
-        benchmark = self.benchmarks.get(method)
-        if benchmark is None:
-            return None
-        test_mean, weight = (benchmark["mean"], benchmark["weight"],)
-        return ((value / test_mean) - 1.0) * weight + 1.0
-
-
-    def _summarizeData(self, operation, data):
-        data = SummarizingMixin._summarizeData(self, operation, data)
-        value = self.qos_value(operation, data[-4])
-        if value is None:
-            value = 0.0
-        return data[:-1] + (value,) + data[-1:]
-
-
-    def report(self, output):
-        output.write("\n")
-        output.write("** REPORT **\n")
-        output.write("\n")
-        runtime = datetime.now() - self._startTime
-        cpu = os.times()
-        cpuUser = cpu[0] + cpu[2]
-        cpuSys = cpu[1] + cpu[3]
-        cpuTotal = cpuUser + cpuSys
-        runHours, remainder = divmod(runtime.seconds, 3600)
-        runMinutes, runSeconds = divmod(remainder, 60)
-        cpuHours, remainder = divmod(cpuTotal, 3600)
-        cpuMinutes, cpuSeconds = divmod(remainder, 60)
-        items = {
-            'Users': self.countUsers(),
-            'Clients': self.countClients(),
-            'Start time': self._startTime.strftime('%m/%d %H:%M:%S'),
-            'Run time': "%02d:%02d:%02d" % (runHours, runMinutes, runSeconds),
-            'CPU Time': "user %-5.2f sys %-5.2f total %02d:%02d:%02d" % (cpuUser, cpuSys, cpuHours, cpuMinutes, cpuSeconds,),
-            'QoS': self.qos(),
-        }
-        if self.countClientFailures() > 0:
-            items['Failed clients'] = self.countClientFailures()
-            for ctr, reason in enumerate(self._failed_clients, 1):
-                items['Failure #%d' % (ctr,)] = reason
-        if self.countSimFailures() > 0:
-            for reason, count in self._failed_sim.items():
-                items['Failed operation'] = "%s : %d times" % (reason, count,)
-        output.write("* Client\n")
-        self.printMiscellaneous(output, items)
-        output.write("\n")
-
-        if self._expired_data is not None:
-            items = {
-                "Req/sec" : "%.1f" % (self._expired_data[0],),
-                "Response": "%.1f (ms)" % (self._expired_data[1],),
-                "Slots": "%.2f" % (self._expired_data[2],),
-                "CPU": "%.1f%%" % (self._expired_data[3],),
-            }
-            output.write("* Server (Last 5 minutes)\n")
-            self.printMiscellaneous(output, items)
-            output.write("\n")
-        output.write("* Details\n")
-
-        self.printHeader(output, [
-            (label, width)
-            for (label, width, _ignore_fmt)
-            in self._fields
-        ])
-        self.printData(
-            output,
-            [fmt for (label, width, fmt) in self._fields],
-            sorted(self._perMethodTimes.items())
-        )
-
-    _FAILED_REASON = "Greater than %(cutoff)g%% %(method)s failed"
-
-    _REASON_1 = "Greater than %(cutoff)g%% %(method)s exceeded "
-    _REASON_2 = "%g second response time"
-
-    def failures(self):
-        # TODO
-        reasons = []
-
-        for (method, times) in self._perMethodTimes.iteritems():
-            failures = 0
-            overDurations = [0] * len(self._thresholds)
-
-            for success, duration in times:
-                if not success:
-                    failures += 1
-                for ctr, item in enumerate(self._thresholds):
-                    threshold, _ignore_fail_at = item
-                    if duration > threshold:
-                        overDurations[ctr] += 1
-
-            checks = [
-                (failures, self._fail_cut_off, self._FAILED_REASON),
-            ]
-
-            for ctr, item in enumerate(self._thresholds):
-                threshold, fail_at = item
-                fail_at = fail_at.get(method, fail_at["default"])
-                checks.append(
-                    (overDurations[ctr], fail_at, self._REASON_1 + self._REASON_2 % (threshold,))
-                )
-
-            for count, cutoff, reason in checks:
-                if count * 100.0 / len(times) > cutoff:
-                    reasons.append(reason % dict(method=method, cutoff=cutoff))
-
-        if self.countClientFailures() != 0:
-            reasons.append("Client failures: %d" % (self.countClientFailures(),))
-        if self.countSimFailures() != 0:
-            reasons.append("Overall failures: %d" % (self.countSimFailures(),))
-        return reasons
-
-
-
 def main():
     import random
 
@@ -626,6 +359,9 @@
     from twisted.python.failure import startDebugMode
     startDebugMode()
 
+    from contrib.performance.loadtest.clients import OS_X_10_6
+    from contrib.performance.loadtest.logger import ReportStatistics, SimpleStatistics, RequestLogger
+
     report = ReportStatistics()
     addObserver(SimpleStatistics().observe)
     addObserver(report.observe)

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/profiles.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/profiles.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/profiles.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -21,30 +21,50 @@
 
 from __future__ import division
 
-import json
 import random
-import sys
 from uuid import uuid4
+from numbers import Number
 
 from caldavclientlibrary.protocol.caldav.definitions import caldavxml
 
 from twisted.python import context
 from twisted.python.log import msg
 from twisted.python.failure import Failure
-from twisted.internet.defer import Deferred, succeed, fail
+from twisted.internet.defer import Deferred, DeferredList, succeed, fail
 from twisted.internet.task import LoopingCall
 from twisted.web.http import PRECONDITION_FAILED
 
-from twistedcaldav.ical import Property, Component
+from twistedcaldav.ical import Property
 
-from contrib.performance.stats import NearFutureDistribution, NormalDistribution, UniformDiscreteDistribution, mean, median
-from contrib.performance.stats import LogNormalDistribution, RecurrenceDistribution
-from contrib.performance.loadtest.logger import SummarizingMixin
+from contrib.performance.loadtest.distributions import (
+    NearFutureDistribution, NormalDistribution, UniformDiscreteDistribution, BernoulliDistribution,
+    LogNormalDistribution, RecurrenceDistribution, FixedDistribution
+)
 from contrib.performance.loadtest.ical import IncorrectResponseCode
+from contrib.performance.loadtest.resources import Calendar, Event
+from contrib.performance.loadtest.templates import eventTemplate, alarmTemplate, taskTemplate
 
 from pycalendar.datetime import DateTime
 from pycalendar.duration import Duration
+from pycalendar.value import Value
 
+def _loopWithDistribution(reactor, distribution, function):
+    result = Deferred()
+    # print(distribution)
+
+    def repeat(ignored):
+        reactor.callLater(distribution.sample(), iterate)
+
+    def iterate():
+        d = function()
+        if d is not None:
+            d.addCallbacks(repeat, result.errback)
+        else:
+            repeat(None)
+
+    repeat(None)
+    return result
+
 class ProfileBase(object):
     """
     Base class which provides some conveniences for profile
@@ -52,27 +72,38 @@
     """
     random = random
 
-    def __init__(self, reactor, simulator, client, userNumber, **params):
+    def __init__(self, enabled, interval, **params):
+        self.enabled = enabled
+        if isinstance(interval, Number):
+            interval = FixedDistribution(interval)
+        self._interval = interval
+        self._params = params
+        self.setDistributions(**params)
+        self._initialized = False
+
+    def duplicate(self):
+        return type(self)(enabled=self.enabled, interval=self._interval, **self._params)
+
+    def setUp(self, reactor, simulator, client, record):
         self._reactor = reactor
         self._sim = simulator
         self._client = client
-        self._number = userNumber
-        self.setParameters(**params)
+        self._record = record
+        self._initialized = True
 
-
-    def setParameters(self):
+    def setDistributions(self):
         pass
 
+    def _wrapper(self):
+        if not self.enabled:
+            return succeed(None)
+        if not self._client.started:
+            return succeed(None)
+        return self.action()
 
-    def initialize(self):
-        """
-        Called before the profile runs for real. Can be used to initialize client state.
+    def run(self):
+        return _loopWithDistribution(self._reactor, self._interval, self._wrapper)
 
-        @return: a L{Deferred} that fires when initialization is done
-        """
-        return succeed(None)
-
-
     def _calendarsOfType(self, calendarType, componentType):
         return [
             cal
@@ -90,6 +121,46 @@
         return attendee.parameterValue('EMAIL') == self._client.email[len('mailto:'):]
 
 
+    def _getRandomCalendarOfType(self, component_type):
+        """
+        Return a random L{Calendar} object from the current user
+        or C{None} if there are no calendars to work with
+        """
+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
+        if not calendars: # Oh no! There are no calendars to play with
+            return None
+        # Choose a random calendar
+        calendar = self.random.choice(calendars)
+        return calendar
+
+
+    def _getRandomEventOfType(self, component_type):
+        """
+        Return a random L{Event} object from the current user
+        or C{None} if there are no events to work with
+        """
+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
+        while calendars:
+            calendar = self.random.choice(calendars)
+            calendars.remove(calendar)
+            if not calendar.events:
+                continue
+
+            events = calendar.events.keys()
+            while events:
+                href = self.random.choice(events)
+                events.remove(href)
+                event = calendar.events[href]
+                if not event.component:
+                    continue
+                return event
+        return None
+
+
+    def _getRandomLocation(self):
+        pass
+
+
     def _newOperation(self, label, deferred):
         """
         Helper to emit a log event when a new operation is started and
@@ -156,46 +227,336 @@
     pass
 
 
+#####################
+# Eventer Hierarchy #
+# ----------------- #
+# EventBase         #
+#   Eventer         #
+#   EventUpdaterBase#
+#     Titler        #
+#     Noter         #
+#     Linker        #
+#     Relocater     #
+#     Repeater      #
+#     Rescheduler   #
+#     ~Alerter~     #
+#     Attacher      #
+#     InviterBase   #
+#       Inviter     #
+#       Relocater   #
+#   EventDeleter    #
+#####################
 
-def loopWithDistribution(reactor, distribution, function):
-    result = Deferred()
+class EventBase(ProfileBase):
+    """
+    Base profile for a calendar user who interacts with events
+    """
+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VEVENT')
 
-    def repeat(ignored):
-        reactor.callLater(distribution.sample(), iterate)
+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VEVENT')
 
 
-    def iterate():
-        d = function()
-        if d is not None:
-            d.addCallbacks(repeat, result.errback)
-        else:
-            repeat(None)
+class Eventer(EventBase):
+    """
+    A Calendar user who creates new events.
+    """
+    def setDistributions(
+        self,
+        eventStartDistribution=NearFutureDistribution(),
+        eventDurationDistribution=UniformDiscreteDistribution([
+            15 * 60, 30 * 60,
+            45 * 60, 60 * 60,
+            120 * 60
+        ])
+    ):
+        """
+        @param eventStartDistribution: Generates datetimes at which an event starts
+        @param eventDurationDistribution: Generates length of event (in seconds)
+        """
+        self._eventStart = eventStartDistribution
+        self._eventDuration = eventDurationDistribution
 
-    repeat(None)
-    return result
+    def _addEvent(self):
+        # Choose a random calendar on which to add an event
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return succeed(None)
+        # print "Going to add an event"
 
+        # Form a new event by modifying fields of the template event
+        vcalendar = eventTemplate.duplicate()
+        vevent = vcalendar.mainComponent()
+        uid = str(uuid4())
+        dtstart = self._eventStart.sample()
+        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
 
+        vevent.replaceProperty(Property("UID", uid))
+        vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
+        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
+        vevent.replaceProperty(Property("DTSTART", dtstart))
+        vevent.replaceProperty(Property("DTEND", dtend))
 
-class Inviter(ProfileBase):
+        href = '%s%s.ics' % (calendar.url, uid)
+        # print("Vcalendar is", vcalendar)
+        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
+        # print("ABout to add event", event.component)
+        d = self._client.addEvent(href, event)
+        return self._newOperation("create{event}", d)
+
+    action = _addEvent
+
+# Could have better handling for not changing events once they're modified
+# esp re: repeating
+class EventUpdaterBase(EventBase):
+    """Superclass of all event mixins.
+    Accepts two parameters
+    enabled: bool on or off
+    interval: distibution that generates integers representing delays
     """
-    A Calendar user who invites and de-invites other users to events.
-    """
-    def setParameters(
+    def action(self):
+        event = self._getRandomEvent()
+        if not event:
+            return succeed(None)
+        component = event.component
+        vevent = component.mainComponent()
+
+        label = self.modifyEvent(event.url, vevent)
+        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
+
+        event.component = component
+        return self._client.updateEvent(event)
+        # d.addCallback(finish)
+
+        return self._newOperation(label, d)
+
+    def modifyEvent(self, href, vevent):
+        """Overriden by subclasses"""
+        pass
+
+class Titler(EventUpdaterBase):
+    def setDistributions(
         self,
-        enabled=True,
-        sendInvitationDistribution=NormalDistribution(600, 60),
-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11))
+        titleLengthDistribution=NormalDistribution(10, 2)
     ):
-        self.enabled = enabled
-        self._sendInvitationDistribution = sendInvitationDistribution
-        self._inviteeDistribution = inviteeDistribution
+        self._titleLength = titleLengthDistribution
 
+    def modifyEvent(self, _ignore_href, vevent):
+        length = max(5, int(self._titleLength.sample()))
+        vevent.replaceProperty(Property("SUMMARY", "Event" + "." * (length - 5)))
+        return "update{title}"
 
-    def run(self):
-        return loopWithDistribution(
-            self._reactor, self._sendInvitationDistribution, self._invite)
+class Transparenter(EventUpdaterBase):
+    def setDistributions(
+        self,
+        transparentLikelihoodDistribution=BernoulliDistribution(0.95)
+    ):
+        self._transparentLikelihood = transparentLikelihoodDistribution
 
+    def modifyEvent(self, _ignore_href, vevent):
+        if self._transparentLikelihood.sample():
+            transparency = "TRANSPARENT"
+        else:
+            transparency = "OPAQUE"
+        vevent.replaceProperty(Property("TRANSP", transparency))
+        return "update{transp}"
 
+class Hider(EventUpdaterBase):
+    def setDistributions(
+        self,
+        publicLikelihoodDistribution=BernoulliDistribution(0.95)
+    ):
+        self._publicLikelihood = publicLikelihoodDistribution
+
+    def modifyEvent(self, _ignore_href, vevent):
+        if self._publicLikelihood.sample():
+            privacy = "PUBLIC"
+        else:
+            privacy = "CONFIDENTIAL"
+        vevent.replaceProperty(Property("X-CALENDARSERVER-ACCESS", privacy))
+        return "update{privacy}"
+
+class Noter(EventUpdaterBase):
+    def setDistributions(
+        self,
+        noteLengthDistribution=NormalDistribution(10, 2)
+    ):
+        self._noteLength = noteLengthDistribution
+
+    def modifyEvent(self, _ignore_href, vevent):
+        length = max(5, int(self._noteLength.sample()))
+        vevent.replaceProperty(Property("DESCRIPTION", "." * length))
+        return "update{notes}"
+
+class Linker(EventUpdaterBase):
+    def setDistributions(
+        self,
+        urlLengthDistribution=NormalDistribution(10, 2)
+    ):
+        self._urlLength = urlLengthDistribution
+
+    def modifyEvent(self, _ignore_href, vevent):
+        length = max(5, int(self._urlLength.sample()))
+        vevent.replaceProperty(Property("URL", 'https://bit.ly/' + '*' * length, valuetype=Value.VALUETYPE_URI))
+        return "update{url}"
+
+class Repeater(EventUpdaterBase):
+    def setDistributions(
+        self,
+        recurrenceDistribution=RecurrenceDistribution(False)
+    ):
+        self._recurrence = recurrenceDistribution
+
+    def modifyEvent(self, _ignore_href, vevent):
+        rrule = self._recurrence.sample()
+        if rrule is not None:
+            vevent.replaceProperty(Property(None, None, None, pycalendar=rrule))
+        return "update{rrule}"
+
+class Rescheduler(EventUpdaterBase):
+    def setDistributions(
+        self,
+        eventStartDistribution=NearFutureDistribution(),
+        eventDurationDistribution=UniformDiscreteDistribution([
+            15 * 60, 30 * 60,
+            45 * 60, 60 * 60,
+            120 * 60
+        ])
+    ):
+        self._eventStart = eventStartDistribution
+        self._eventDuration = eventDurationDistribution
+
+    def modifyEvent(self, _ignore_href, vevent):
+        dtstart = self._eventStart.sample()
+        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
+        vevent.replaceProperty(Property("DTSTART", dtstart))
+        vevent.replaceProperty(Property("DTEND", dtend))
+        return "reschedule{event}"
+
+# class Alerter(EventUpdaterBase):
+# component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC()))
+#     pass
+
+class Attacher(EventUpdaterBase):
+    def setDistributions(
+        self,
+        filesizeDistribution=NormalDistribution(24, 3),
+        numAttachmentsDistribution=LogNormalDistribution(2, 1),
+        attachLikelihoodDistribution=BernoulliDistribution(0.9),
+
+    ):
+        self._filesize = filesizeDistribution
+        # self._numAttachments = numAttachmentsDistribution
+        # self._attachLikelihood = attachLikelihoodDistribution
+        # pass
+
+    def modifyEvent(self, href, vevent):
+        d = self._client.postAttachment(href, 'x' * 1024)
+        return "attach{files}"
+
+    def handleAttachments(self):
+        pass
+
+        # if True: # attachLikelihoodDistribution.sample():
+        #     # size = max(0, int(self._filesize.sample()))
+        #     numAttachments()
+        #     self.attachFiles(event, filesizeDistribution.sample())
+        # else:
+        #     pass
+
+    def attachFile(self, event):
+        # PUT new event information (nothing has actually changed)
+        # POST attachment (with Content-Disposition header, and response location)
+        # GET updated event
+        pass
+
+    def unattachFile(self):
+        pass
+
+class InviterBase(EventUpdaterBase):
+    def setDistributions(
+        self,
+        numInviteesDistribution=NormalDistribution(7, 2)
+    ):
+        self._numInvitees = numInviteesDistribution
+
+    def _findUninvitedRecord(self, vevent):
+        pass
+
+    def _addAttendee(self, some_id):
+        attendeeProp = self._buildAttendee()
+        self._client.attendeeAutocomplete
+
+    # def _didSelfOrganize(self, vevent):
+
+    # TODO handle alternate roles
+    def _buildAttendee(self, commonname, cuaddr, isIndividual=True, isRequired=False):
+        return Property(
+            name=u'ATTENDEE',
+            value=cuaddr.encode("utf-8"),
+            params={
+                'CN': commonname,
+                'CUTYPE': 'INDIVIDUAL' if isIndividual else 'ROOM',
+                'PARTSTAT': 'NEEDS-ACTION',
+                'ROLE': 'REQ-PARTICIPANT',
+                'RSVP': 'TRUE',
+            },
+        )
+
+    def _getAttendees(self, vevent):
+        return vevent.properties('ATTENDEE')
+
+    def _invite():
+        raise NotImplementedError
+
+    def _addAttendee():
+        raise NotImplementedError
+
+class Inviter(InviterBase):
+    def modifyEvent(self, href, vevent):
+        # print("*" * 16)
+        numToInvite = max(0, int(self._numInvitees.sample()))
+        deferreds = []
+        for _ignore_i in xrange(numToInvite):
+            number = random.randint(1, 50)
+            record = self._sim.getUserRecord(number)
+            attendee = self._buildAttendee(record.commonName, record.email)
+            deferreds.append(self._client.addEventAttendee(href, attendee))
+            vevent.addProperty(attendee)
+        # d = self._client.addInvite(event)
+        # deferreds.append(d)
+        return DeferredList(deferreds)
+
+
+    def test(self):
+        event = self._getRandomEvent()
+        if not event:
+            return succeed(None)
+        href = event.url
+
+        attendee = Property(
+            name=u'ATTENDEE',
+            value='urn:uuid:30000000-0000-0000-0000-000000000002',
+            params={
+                'CN': 'Location 02',
+                'CUTYPE': 'ROOM',
+                'PARTSTAT': 'NEEDS-ACTION',
+                'ROLE': 'REQ-PARTICIPANT',
+                'RSVP': 'TRUE',
+            },
+        )
+
+        d = self._client.addEventAttendee(href, attendee)
+
+        component = event.component
+        component.mainComponent().addProperty(attendee)
+        event.component = component
+
+        d2 = self._client.addInvite(event)
+        return self._newOperation("add attendee", DeferredList([d, d2]))
+
+
     def _addAttendee(self, event, attendees):
         """
         Create a new attendee to add to the list of attendees for the
@@ -280,7 +641,8 @@
                 # Find out who might attend
                 attendees = tuple(component.properties('ATTENDEE'))
 
-                d = self._addAttendee(event, attendees)
+                # d = self._addAttendee(event, attendees)
+                d = self._addLocation(event, "Location 05", "urn:uuid:30000000-0000-0000-0000-000000000005")
                 d.addCallbacks(
                     lambda attendee:
                         self._client.addEventAttendee(
@@ -291,173 +653,248 @@
         # Oops, either no events or no calendars to play with.
         return succeed(None)
 
+    # action = invite
 
+class Relocater(InviterBase):
+    def setDistributions(
+        self,
+    ):
+        pass
 
-class RealisticInviter(ProfileBase):
+class EventDeleter(EventBase):
     """
-    A Calendar user who invites other users to new events.
+    A calendar user who deletes events at random
     """
-    _eventTemplate = Component.fromString("""\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20101018T155431Z
-UID:C98AD237-55AD-4F7D-9009-0D355D835822
-DTEND;TZID=America/New_York:20101021T130000
-TRANSP:OPAQUE
-SUMMARY:Simple event
-DTSTART;TZID=America/New_York:20101021T120000
-DTSTAMP:20101018T155438Z
-SEQUENCE:2
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"))
+    def _deleteEvent(self, event):
+        # if self organized
+        d = self._client.deleteEvent(event.url)
+        return self._newOperation("delete{event}", d)
+        # if am attendee
 
+    def _deleteRandomEvent(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
+        return self._deleteEvent(event)
 
-    def setParameters(
-        self,
-        enabled=True,
-        sendInvitationDistribution=NormalDistribution(600, 60),
-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11)),
-        inviteeClumping=True,
-        inviteeCountDistribution=LogNormalDistribution(1.2, 1.2),
-        eventStartDistribution=NearFutureDistribution(),
-        eventDurationDistribution=UniformDiscreteDistribution([
-            15 * 60, 30 * 60,
-            45 * 60, 60 * 60,
-            120 * 60
-        ]),
-        recurrenceDistribution=RecurrenceDistribution(False),
-    ):
-        self.enabled = enabled
-        self._sendInvitationDistribution = sendInvitationDistribution
-        self._inviteeDistribution = inviteeDistribution
-        self._inviteeClumping = inviteeClumping
-        self._inviteeCountDistribution = inviteeCountDistribution
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
-        self._recurrenceDistribution = recurrenceDistribution
+    action = _deleteRandomEvent
 
 
+
+class Emptier(EventDeleter):
+    """
+    Behavior that keep events underneath capacity
+    """
+    MAX_RESOURCES_PER_COLLECTION = 10
+    MAX_PERCENT_FULL = 0.9
+
     def run(self):
-        return loopWithDistribution(
-            self._reactor, self._sendInvitationDistribution, self._invite)
+        deferreds = []
+        for calendar in self._client._calendars.values():
+            deferreds = []
+            numToDelete = max(0, int(len(calendar.events) - self.MAX_PERCENT_FULL * self.MAX_RESOURCES_PER_COLLECTION))
+            events = calendar.events.values()
+            print(events)
+            eventsToDelete = events[:numToDelete]
+            if eventsToDelete:
+                print("*" * 16)
+                print("Deleting an event because of capacity")
+                for event in eventsToDelete:
+                    d = self._deleteEvent(event)
+                    deferreds.append(d)
+        if not deferreds:
+            return succeed(None)
+        return self._newOperation("empty{calendar}", DeferredList(deferreds))
 
 
-    def _addAttendee(self, event, attendees):
-        """
-        Create a new attendee to add to the list of attendees for the
-        given event.
-        """
-        selfRecord = self._sim.getUserRecord(self._number)
-        invitees = set([u'mailto:%s' % (selfRecord.email,)])
-        for att in attendees:
-            invitees.add(att.value())
 
-        for _ignore_i in range(10):
 
-            sample = self._inviteeDistribution.sample()
-            if self._inviteeClumping:
-                sample = self._number + sample
-            invitee = max(0, sample)
+""" TEST """
+# class Intern(object):
+#     def __init__(self):
+#         self.behaviors = [
+#             Eventer(asdfjadsf),
+#             Attacher(asjadsfjasdf),
+#             Inviter(enabled=True, **params)
+#         ]
 
-            try:
-                record = self._sim.getUserRecord(invitee)
-            except IndexError:
-                continue
-            cuaddr = u'mailto:%s' % (record.email,)
-            if cuaddr not in invitees:
-                break
-        else:
-            raise CannotAddAttendee("Can't find uninvited user to invite.")
+#     def run(self):
+#         deferreds = []
+#         for behavior in self.behaviors:
+#             deferreds.append(behavior.run())
+#         return DeferredList(deferreds)
 
-        attendee = Property(
-            name=u'ATTENDEE',
-            value=cuaddr.encode("utf-8"),
-            params={
-                'CN': record.commonName,
-                'CUTYPE': 'INDIVIDUAL',
-                'PARTSTAT': 'NEEDS-ACTION',
-                'ROLE': 'REQ-PARTICIPANT',
-                'RSVP': 'TRUE',
-            },
-        )
 
-        event.addProperty(attendee)
-        attendees.append(attendee)
 
+####################
+# Tasker Hierarchy #
+# ---------------- #
+# TaskBase         #
+#   Tasker         #
+#   TaskDeleter    #
+#   TaskUpdaterBase#
+#     Titler       #
+#     Noter        #
+#     Prioritizer  #
+#     Completer    #
+#     Alerter      #
+####################
 
-    def _invite(self):
-        """
-        Try to add a new event, or perhaps remove an
-        existing attendee from an event.
 
-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        """
+class TaskBase(ProfileBase):
+    """
+    Base profile for a calendar user who interacts with tasks
+    """
+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VTODO')
 
-        if not self._client.started:
+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VTODO')
+
+
+class Tasker(TaskBase):
+    """
+    A Calendar user who creates new tasks.
+    """
+    def _addTask(self):
+        calendar = self._getRandomCalendar()
+        if not calendar:
             return succeed(None)
 
-        # Find calendars which are eligible for invites
-        calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT")
+        # Form a new event by modifying fields of the template event
+        vcalendar = taskTemplate.duplicate()
+        vtodo = vcalendar.mainComponent()
+        uid = str(uuid4())
 
-        while calendars:
-            # Pick one at random from which to try to create an event
-            # to modify.
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
+        vtodo.replaceProperty(Property("UID", uid))
+        vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
 
-            # Copy the template event and fill in some of its fields
-            # to make a new event to create on the calendar.
-            vcalendar = self._eventTemplate.duplicate()
-            vevent = vcalendar.mainComponent()
-            uid = str(uuid4())
-            dtstart = self._eventStartDistribution.sample()
-            dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-            vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTART", dtstart))
-            vevent.replaceProperty(Property("DTEND", dtend))
-            vevent.replaceProperty(Property("UID", uid))
+        href = '%s%s.ics' % (calendar.url, uid)
+        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
+        d = self._client.addEvent(href, event)
+        return self._newOperation("create{task}", d)
 
-            rrule = self._recurrenceDistribution.sample()
-            if rrule is not None:
-                vevent.addProperty(Property(None, None, None, pycalendar=rrule))
+    action = _addTask
 
-            vevent.addProperty(self._client._makeSelfOrganizer())
-            vevent.addProperty(self._client._makeSelfAttendee())
+class TaskDeleter(TaskBase):
+    def _deleteTask(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
 
-            attendees = list(vevent.properties('ATTENDEE'))
-            for _ignore in range(int(self._inviteeCountDistribution.sample())):
-                try:
-                    self._addAttendee(vevent, attendees)
-                except CannotAddAttendee:
-                    self._failedOperation("invite", "Cannot add attendee")
-                    return succeed(None)
+        d = self._client.deleteEvent(event.url)
+        return self._newOperation("delete{task}", d)
 
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addInvite(href, vcalendar)
-            return self._newOperation("invite", d)
+    action = _deleteTask
 
 
+class TaskUpdaterBase(TaskBase):
+    def action(self):
+        task = self._getRandomEvent()
+        if not task:
+            return succeed(None)
+        component = task.component
+        vtodo = component.mainComponent()
 
+        label = self.modifyEvent(task.url, vtodo)
+        vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
+
+        task.component = component
+        d = self._client.updateEvent(task, method_label="update{task}")
+        return self._newOperation(label, d)
+
+    def modifyEvent(self, href, vtodo):
+        """Overriden by subclasses"""
+        pass
+
+
+class TaskTitler(TaskUpdaterBase, Titler):
+    """
+    Changes the SUMMARY of a random VTODO
+    """
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property("SUMMARY", "." * 5))
+        return "update{title}"
+
+class TaskNoter(TaskUpdaterBase, Noter):
+    """
+    Changes the NOTES of a random VTODO
+    """
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property("DESCRIPTION", "." * 5))
+        return "update{notes}"
+
+
+# class TaskAlerterMixin = AlerterMixin (alarm AND due)
+# self._taskStartDistribution = taskDueDistribution
+# vtodo.replaceProperty(Property("DUE", due))
+
+
+class Prioritizer(TaskUpdaterBase):
+    PRIORITY_NONE = 0
+    PRIORITY_HIGH = 1
+    PRIORITY_MEDIUM = 5
+    PRIORITY_LOW = 9
+
+    def setDistributions(
+        self,
+        priorityDistribution=UniformDiscreteDistribution([
+            PRIORITY_NONE, PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH
+        ])
+    ):
+        self._priority = priorityDistribution
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        self._setPriority(vtodo, self._priority.sample())
+
+
+    def _setPriority(self, vtodo, priority):
+        """ Set the PRIORITY of a VTODO """
+        vtodo.replaceProperty(Property("PRIORITY", priority))
+
+class Completer(TaskUpdaterBase):
+    def setDistributions(
+        self,
+        completeLikelihood=BernoulliDistribution(0.9)
+    ):
+        self._complete = completeLikelihood
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        if self._complete.sample():
+            self._markTaskComplete(vtodo)
+        else:
+            self._markTaskIncomplete(vtodo)
+
+    def _markTaskComplete(self, vtodo):
+        """ Mark a VTODO as complete """
+        vtodo.replaceProperty(Property("COMPLETED", DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property("PERCENT-COMPLETE", 100))
+        vtodo.replaceProperty(Property("STATUS", "COMPLETED"))
+
+    def _markTaskIncomplete(self, vtodo):
+        """ Mark a VTODO as incomplete """
+        vtodo.removeProperty("COMPLETED")
+        vtodo.removeProperty("PERCENT-COMPLETE")
+        vtodo.replaceProperty(Property("STATUS", "NEEDS-ACTION"))
+
+
+##########################
+# Notification Behaviors #
+##########################
 class Accepter(ProfileBase):
     """
     A Calendar user who accepts invitations to events. As well as accepting requests, this
     will also remove cancels and replies.
     """
-    def setParameters(
+    def setDistributions(
         self,
-        enabled=True,
-        acceptDelayDistribution=NormalDistribution(1200, 60)
+        acceptDelayDistribution=NormalDistribution(1200, 60),
+        acceptLikelihoodDistribution=BernoulliDistribution(1),
     ):
-        self.enabled = enabled
         self._accepting = set()
         self._acceptDelayDistribution = acceptDelayDistribution
+        self._acceptLikelihood = acceptLikelihoodDistribution
 
 
     def run(self):
@@ -474,13 +911,14 @@
         except KeyError:
             return
 
-        if calendar.resourceType == caldavxml.schedule_inbox:
-            # Handle inbox differently
-            self.inboxEventChanged(calendar, href)
-        elif calendar.resourceType == caldavxml.calendar:
-            self.calendarEventChanged(calendar, href)
-        else:
-            return
+        if self._acceptLikelihood.sample():
+            if calendar.resourceType == caldavxml.schedule_inbox:
+                # Handle inbox differently
+                self.inboxEventChanged(calendar, href)
+            elif calendar.resourceType == caldavxml.calendar:
+                self.calendarEventChanged(calendar, href)
+            else:
+                return
 
 
     def calendarEventChanged(self, calendar, href):
@@ -533,7 +971,7 @@
 
             # Download the event again and attempt to make the change
             # to the attendee list again.
-            d = self._client.updateEvent(href)
+            d = self._client._refreshEvent(href)
             def cbUpdated(ignored):
                 d = change()
                 d.addErrback(scheduleError)
@@ -597,399 +1035,162 @@
 
 
 
-class Eventer(ProfileBase):
+######################
+# Calendar Behaviors #
+######################
+class CalendarBase(ProfileBase):
     """
-    A Calendar user who creates new events.
+    A calendar user who interacts with calendars
     """
-    _eventTemplate = Component.fromString("""\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20101018T155431Z
-UID:C98AD237-55AD-4F7D-9009-0D355D835822
-DTEND;TZID=America/New_York:20101021T130000
-TRANSP:OPAQUE
-SUMMARY:Simple event
-DTSTART;TZID=America/New_York:20101021T120000
-DTSTAMP:20101018T155438Z
-SEQUENCE:2
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"))
+    # def initialize(self):
+    #     self.action = lambda: None
+    #     return succeed(None)
 
-    def setParameters(
-        self,
-        enabled=True,
-        interval=25,
-        eventStartDistribution=NearFutureDistribution(),
-        eventDurationDistribution=UniformDiscreteDistribution([
-            15 * 60, 30 * 60,
-            45 * 60, 60 * 60,
-            120 * 60
-        ]),
-        recurrenceDistribution=RecurrenceDistribution(False),
-    ):
-        self.enabled = enabled
-        self._interval = interval
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
-        self._recurrenceDistribution = recurrenceDistribution
 
+    # def setDistributions(self, enabled=True, interval=25):
+    #     self.enabled = enabled
+    #     self._interval = interval
 
-    def run(self):
-        self._call = LoopingCall(self._addEvent)
-        self._call.clock = self._reactor
-        return self._call.start(self._interval)
 
 
-    def _addEvent(self):
-        if not self._client.started:
-            return succeed(None)
+class CalendarMaker(CalendarBase):
+    """ A Calendar user who adds new Calendars """
 
-        calendars = self._calendarsOfType(caldavxml.calendar, "VEVENT")
+    def _addCalendar(self):
+        print "Adding a calendar"
+        # if not self._client.started:
+        #     return None
 
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
+        # uid = str(uuid4())
 
-            # Copy the template event and fill in some of its fields
-            # to make a new event to create on the calendar.
-            vcalendar = self._eventTemplate.duplicate()
-            vevent = vcalendar.mainComponent()
-            uid = str(uuid4())
-            dtstart = self._eventStartDistribution.sample()
-            dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-            vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-            vevent.replaceProperty(Property("DTSTART", dtstart))
-            vevent.replaceProperty(Property("DTEND", dtend))
-            vevent.replaceProperty(Property("UID", uid))
+        # body = Calendar.buildCalendarXML(order=0, component_type="VEVENT", rgba_color='FB524FFF', name='Sample Calendar')
+        # print("Making new calendar with uid: " + uid)
+        # # XXX Just for testing! remove this soon
+        # path = "/calendars/__uids__/" + self._client.record.guid + "/" + uid + "/"
+        # d = self._client.addCalendar(path, body)
+        d = succeed('calendar created')
+        return self._newOperation("create", d)
 
-            rrule = self._recurrenceDistribution.sample()
-            if rrule is not None:
-                vevent.addProperty(Property(None, None, None, pycalendar=rrule))
+    action = _addCalendar
 
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation("create", d)
 
 
-
-class EventUpdater(ProfileBase):
+class CalendarUpdater(CalendarBase):
     """
-    A Calendar user who creates a new event, and then updates its alarm.
+    A calendar user who updates random calendars
     """
-    _eventTemplate = Component.fromString("""\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20101018T155431Z
-UID:C98AD237-55AD-4F7D-9009-0D355D835822
-DTEND;TZID=America/New_York:20101021T130000
-TRANSP:OPAQUE
-SUMMARY:Simple event
-DTSTART;TZID=America/New_York:20101021T120000
-DTSTAMP:20101018T155438Z
-SEQUENCE:2
-BEGIN:VALARM
-X-WR-ALARMUID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
-UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
-DESCRIPTION:Event reminder
-TRIGGER:-PT8M
-ACTION:DISPLAY
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n"))
+    def initialize(self):
+        from collections import defaultdict
+        self.action = self._updateCalendar
+        self._calendarModCount = defaultdict(int) # Map from calendar href to count of modifications
+        return succeed(None)
 
-    def setParameters(
-        self,
-        enabled=True,
-        interval=25,
-        eventStartDistribution=NearFutureDistribution(),
-        eventDurationDistribution=UniformDiscreteDistribution([
-            15 * 60, 30 * 60,
-            45 * 60, 60 * 60,
-            120 * 60
-        ]),
-        recurrenceDistribution=RecurrenceDistribution(False),
-    ):
-        self.enabled = enabled
-        self._interval = interval
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
-        self._recurrenceDistribution = recurrenceDistribution
+    def _updateCalendar(self):
+        if not self._client.started:
+            return None
 
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return None
 
-    def initialize(self):
-        """
-        Called before the profile runs for real. Can be used to initialize client state.
+        self._calendarModCount[calendar.url] += 1
+        modcount = self._calendarModCount[calendar.url]
 
-        @return: a L{Deferred} that fires when initialization is done
-        """
-        return self._initEvent()
+        colors = [
+            "#800000FF", # maroon
+            "#FF0000FF", # red
+            "#008000FF", # green
+            "#00FF00FF", # line
+            "#000080FF", # navy
+            "#0000FFFF", # blue
+        ]
+        color = colors[modcount % len(colors)]
+        self._client.setCalendarDisplayName(calendar, "Calendar ({mods})".format(mods=modcount))
+        self._client.setCalendarColor(calendar, color)
+        # choice = self.random.randint(0, 4)
+        # if choice == 0:
+        #     self._client._
+        # return succeed(None)
 
+    def randomUpdate(self):
+        pass
 
-    def run(self):
-        self._call = LoopingCall(self._updateEvent)
-        self._call.clock = self._reactor
-        return self._call.start(self._interval)
+class CalendarSharer(CalendarBase, InviterBase):
+    """
+    A calendar user who shares random calendars.
+    Even though the real client allows batch requests (e.g. 10 shares in one HTTP request),
+    we simplify life (TODO: keep it real) by having each HTTP request only add or remove one sharee.
+    """
 
+    def initialize(self):
+        self.action = self._shareCalendar
+        return succeed(None)
 
-    def _initEvent(self):
+    def _shareCalendar(self):
         if not self._client.started:
             return succeed(None)
 
-        # If it already exists, don't re-create
-        calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0]
-        if calendar.events:
-            events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")]
-            if events:
-                return succeed(None)
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return None
 
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = self._eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-        vevent.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-        vevent.replaceProperty(Property("DTSTART", dtstart))
-        vevent.replaceProperty(Property("DTEND", dtend))
-        vevent.replaceProperty(Property("UID", uid))
+        # The decision of who to invite / uninvite should be made here
+        inv = random.randint(0, 1)
+        rem = random.randint(0, 1)
 
-        rrule = self._recurrenceDistribution.sample()
-        if rrule is not None:
-            vevent.addProperty(Property(None, None, None, pycalendar=rrule))
+        invRecord = self._sim.getUserRecord(inv)
+        remRecord = self._sim.getUserRecord(rem)
 
-        href = '%s%s' % (calendar.url, "event_to_update.ics")
-        d = self._client.addEvent(href, vcalendar)
-        return self._newOperation("create", d)
+        print("Sharing " + calendar.url)
+        self._inviteUser(calendar, invRecord)
+        # self._removeUser(calendar, remRecord)
 
+        return succeed(None)
 
-    def _updateEvent(self):
-        """
-        Try to add a new attendee to an event, or perhaps remove an
-        existing attendee from an event.
+    def _inviteUser(self, calendar, userRecord):
+        mailto = "mailto:{}".format(userRecord.email)
+        body = Calendar.addInviteeXML(mailto, calendar.name, readwrite=True)
+        d = self._client.postXML(calendar.url, body)
+        # print(body)
 
-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        """
+    def _removeUser(self, calendar, userRecord):
+        mailto = "mailto:{}".format(userRecord.email)
 
-        if not self._client.started:
-            return succeed(None)
+        body = Calendar.removeInviteeXML(mailto)
 
-        # If it does not exist, try to create it
-        calendar = self._calendarsOfType(caldavxml.calendar, "VEVENT")[0]
-        if not calendar.events:
-            return self._initEvent()
-        events = [event for event in calendar.events.values() if event.url.endswith("event_to_update.ics")]
-        if not events:
-            return self._initEvent()
-        event = events[0]
+        d = self._client.postXML(calendar.url, body)
+        # print(body)
 
-        # Add/update the ACKNOWLEDGED property
-        component = event.component.mainComponent()
-        component.replaceProperty(Property("ACKNOWLEDGED", DateTime.getNowUTC()))
-        d = self._client.changeEvent(event.url)
-        return self._newOperation("update", d)
 
 
-
-class Tasker(ProfileBase):
+class CalendarDeleter(CalendarBase):
     """
-    A Calendar user who creates new tasks.
+    A calendar user who deletes entire calendars
     """
-    _taskTemplate = Component.fromString("""\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VTODO
-CREATED:20101018T155431Z
-UID:C98AD237-55AD-4F7D-9009-0D355D835822
-SUMMARY:Simple task
-DUE;TZID=America/New_York:20101021T120000
-DTSTAMP:20101018T155438Z
-END:VTODO
-END:VCALENDAR
-""".replace("\n", "\r\n"))
+    def initialize(self):
+        self.action = self._deleteCalendar
+        return succeed(None)
 
-    def setParameters(
-        self,
-        enabled=True,
-        interval=25,
-        taskDueDistribution=NearFutureDistribution(),
-    ):
-        self.enabled = enabled
-        self._interval = interval
-        self._taskStartDistribution = taskDueDistribution
-
-
-    def run(self):
-        self._call = LoopingCall(self._addTask)
-        self._call.clock = self._reactor
-        return self._call.start(self._interval)
-
-
-    def _addTask(self):
+    def _deleteCalendar(self):
         if not self._client.started:
             return succeed(None)
 
-        calendars = self._calendarsOfType(caldavxml.calendar, "VTODO")
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return None
+        print("Deleting " + calendar.url)
+        d = self._client.deleteCalendar(calendar.url)
+        return self._newOperation("delete", d)
 
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
+if __name__ == '__main__':
+    class TestProfile(ProfileBase):
+        def sayHello(self):
+            print("Hello!")
+        action = sayHello
 
-            # Copy the template task and fill in some of its fields
-            # to make a new task to create on the calendar.
-            vcalendar = self._taskTemplate.duplicate()
-            vtodo = vcalendar.mainComponent()
-            uid = str(uuid4())
-            due = self._taskStartDistribution.sample()
-            vtodo.replaceProperty(Property("CREATED", DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property("DUE", due))
-            vtodo.replaceProperty(Property("UID", uid))
+    from twisted.internet import reactor
 
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation("create", d)
-
-
-
-class OperationLogger(SummarizingMixin):
-    """
-    Profiles will initiate operations which may span multiple requests.  Start
-    and stop log messages are emitted for these operations and logged by this
-    logger.
-    """
-    formats = {
-        u"start" : u"%(user)s - - - - - - - - - - - %(label)8s BEGIN %(lag)s",
-        u"end"   : u"%(user)s - - - - - - - - - - - %(label)8s END [%(duration)5.2f s]",
-        u"failed": u"%(user)s x x x x x x x x x x x %(label)8s FAILED %(reason)s",
-    }
-
-    lagFormat = u'{lag %5.2f ms}'
-
-    # the response time thresholds to display together with failing % count threshold
-    _thresholds_default = {
-        "operations": {
-            "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
-            "thresholds": {
-                "default": [100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0],
-            }
-        }
-    }
-    _lag_cut_off = 1.0      # Maximum allowed median scheduling latency, seconds
-    _fail_cut_off = 1.0     # % of total count at which failed requests will cause a failure
-
-    _fields_init = [
-        ('operation', -25, '%-25s'),
-        ('count', 8, '%8s'),
-        ('failed', 8, '%8s'),
-    ]
-
-    _fields_extend = [
-        ('mean', 8, '%8.4f'),
-        ('median', 8, '%8.4f'),
-        ('stddev', 8, '%8.4f'),
-        ('avglag (ms)', 12, '%12.4f'),
-        ('STATUS', 8, '%8s'),
-    ]
-
-    def __init__(self, outfile=None, **params):
-        self._perOperationTimes = {}
-        self._perOperationLags = {}
-        if outfile is None:
-            outfile = sys.stdout
-        self._outfile = outfile
-
-        # Load parameters from config
-        if "thresholdsPath" in params:
-            jsondata = json.load(open(params["thresholdsPath"]))
-        elif "thresholds" in params:
-            jsondata = params["thresholds"]
-        else:
-            jsondata = self._thresholds_default
-        self._thresholds = [[limit, {}] for limit in jsondata["operations"]["limits"]]
-        for ctr, item in enumerate(self._thresholds):
-            for k, v in jsondata["operations"]["thresholds"].items():
-                item[1][k] = v[ctr]
-
-        self._fields = self._fields_init[:]
-        for threshold, _ignore_fail_at in self._thresholds:
-            self._fields.append(('>%g sec' % (threshold,), 10, '%10s'))
-        self._fields.extend(self._fields_extend)
-
-        if "lagCutoff" in params:
-            self._lag_cut_off = params["lagCutoff"]
-
-        if "failCutoff" in params:
-            self._fail_cut_off = params["failCutoff"]
-
-
-    def observe(self, event):
-        if event.get("type") == "operation":
-            event = event.copy()
-            lag = event.get('lag')
-            if lag is None:
-                event['lag'] = ''
-            else:
-                event['lag'] = self.lagFormat % (lag * 1000.0,)
-
-            self._outfile.write(
-                (self.formats[event[u'phase']] % event).encode('utf-8') + '\n')
-
-            if event[u'phase'] == u'end':
-                dataset = self._perOperationTimes.setdefault(event[u'label'], [])
-                dataset.append((event[u'success'], event[u'duration']))
-            elif lag is not None:
-                dataset = self._perOperationLags.setdefault(event[u'label'], [])
-                dataset.append(lag)
-
-
-    def _summarizeData(self, operation, data):
-        avglag = mean(self._perOperationLags.get(operation, [0.0])) * 1000.0
-        data = SummarizingMixin._summarizeData(self, operation, data)
-        return data[:-1] + (avglag,) + data[-1:]
-
-
-    def report(self, output):
-        output.write("\n")
-        self.printHeader(output, [
-            (label, width)
-            for (label, width, _ignore_fmt) in self._fields
-        ])
-        self.printData(
-            output,
-            [fmt for (label, width, fmt) in self._fields],
-            sorted(self._perOperationTimes.items())
-        )
-
-    _LATENCY_REASON = "Median %(operation)s scheduling lag greater than %(cutoff)sms"
-    _FAILED_REASON = "Greater than %(cutoff).0f%% %(operation)s failed"
-
-    def failures(self):
-        reasons = []
-
-        for operation, lags in self._perOperationLags.iteritems():
-            if median(lags) > self._lag_cut_off:
-                reasons.append(self._LATENCY_REASON % dict(
-                    operation=operation.upper(), cutoff=self._lag_cut_off * 1000))
-
-        for operation, times in self._perOperationTimes.iteritems():
-            failures = len([success for (success, _ignore_duration) in times if not success])
-            if failures * 100.0 / len(times) > self._fail_cut_off:
-                reasons.append(self._FAILED_REASON % dict(
-                    operation=operation.upper(), cutoff=self._fail_cut_off))
-
-        return reasons
+    profile = TestProfile(enabled=True, interval=1)
+    profile.setUp(reactor, None, None, None)
+    profile.run()
+    reactor.run()

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/pubsub.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/pubsub.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/pubsub.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,48 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+"""
+Publisher-Subscription model
+"""
+class _Subscription(object):
+    def __init__(self, publisher, subscriber):
+        self.publisher = publisher
+        self.subscriber = subscriber
+
+
+    def cancel(self):
+        self.publisher.subscriptions.remove(self)
+
+
+    def issue(self, issue):
+        self.subscriber(issue)
+
+
+
+class Publisher(object):
+    def __init__(self):
+        self.subscriptions = []
+
+
+    def subscribe(self, who):
+        subscription = _Subscription(self, who)
+        self.subscriptions.append(subscription)
+        return subscription
+
+
+    def issue(self, issue):
+        for subscr in self.subscriptions:
+            subscr.issue(issue)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/push.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/push.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/push.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,133 @@
+import uuid
+
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.defer import inlineCallbacks, succeed
+
+from calendarserver.push.amppush import SubscribeToID, UnsubscribeFromID, AMPPushClientFactory
+
+class PushMonitor(object):
+    """
+    Watchguard that monitors push notifications (AMP Push)
+    """
+
+    def __init__(
+        self,
+        reactor,
+        ampPushHost,
+        ampPushPort,
+        callback
+    ):
+        """
+        @param reactor: Twisted reactor
+        @type reactor: twisted.web.reactor
+        @param ampPushHost: AMP host to connect to (e.g. 'localhost')
+        @type ampPushHost: string
+        @param ampPushPort: AMP port to connect to (e.g. 62311)
+        @type ampPushPort: integer
+        @param callback: a one-argument function that is fired
+            with a calendar href upon receipt of a push notification
+            for that resource
+        @type callback: one-argument callable
+        """
+
+        if reactor is None:
+            from twisted.internet import reactor
+
+        self._reactor = reactor
+        self._ampPushHost = ampPushHost
+        self._ampPushPort = ampPushPort
+
+        # Keep track of AMP parameters for calendar homes we encounter.  This
+        # dictionary has pushkeys as keys and calendar home URLs as values.
+        self._ampPushkeys = {}
+
+        self._callback = callback
+
+        self._token = str(uuid.uuid4()) # Unique token for this monitor
+        self._endpoint = TCP4ClientEndpoint(self._reactor, self._ampPushHost, self._ampPushPort)
+        self._factory = AMPPushClientFactory(self._receivedAMPPush)
+        self._connected = False
+
+    @inlineCallbacks
+    def begin(self):
+        """
+        Start monitoring for AMP-based push notifications
+        """
+        self._protocol = yield self._endpoint.connect(self._factory)
+        self._connected = True
+        pushkeys = self._ampPushkeys.keys()
+        yield self._subscribeToPushkeys(pushkeys)
+
+    @inlineCallbacks
+    def end(self):
+        """
+        Finish monitoring push notifications.
+        """
+        pushkeys = self._ampPushkeys.keys()
+        self._ampPushkeys = {}
+        yield self._unsubscribeFromPushkeys(pushkeys)
+
+        # Close the connection between client and server
+        yield self._protocol.transport.loseConnection()
+        self._connected = False
+
+
+    def addPushkey(self, pushkey, href):
+        """
+        Register a pushkey associated with a specific calendar href.
+
+        @param pushkey: AMP pushkey returned by the server, used to listen to notifications
+        @type pushkey: C{str}
+        @param href: href of calendar home set. When the server triggers a push for the
+            associated pushkey, the callback will be fired with this href
+        @type href: C{str}
+
+        Example Usage:
+            monitor.addPushkey('/CalDAV/localhost/<uid>', '/calendars/__uids__/<uid>')
+        """
+        self._ampPushkeys[pushkey] = href
+        return self._subscribeToPushkey(pushkey)
+
+    def removePushkey(self, pushkey):
+        """
+        Unregister the calendar home associated with the specified pushkey
+        """
+        if pushkey in self._ampPushkeys:
+            del self._ampPushkeys[pushkey]
+        return self._unsubscribeFromPushkey(pushkey)
+
+    def isSubscribedTo(self, href):
+        """
+        Returns true if and only if the given calendar href is actively being monitored
+        """
+        return href in self._ampPushkeys.itervalues()
+
+    @inlineCallbacks
+    def _subscribeToPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._subscribeToPushkey(pushkey)
+
+    @inlineCallbacks
+    def _unsubscribeFromPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._unsubscribeFromPushkey(pushkey)
+
+    def _subscribeToPushkey(self, pushkey):
+        if not self._connected:
+            return succeed(None)
+        return self._protocol.callRemote(SubscribeToID, token=self._token, id=pushkey)
+
+    def _unsubscribeFromPushkey(self, pushkey):
+        if not self._connected:
+            return succeed(None)
+        return self._protocol.callRemote(UnsubscribeFromID, id=pushkey)
+
+
+    def _receivedAMPPush(self, inboundID, dataChangedTimestamp, priority=5):
+        if inboundID in self._ampPushkeys:
+            # Only react if we're tracking this pushkey
+            href = self._ampPushkeys[inboundID]
+            self._callback(href)
+        else:
+            # Somehow we are not subscribed to this pushkey
+            pass

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/records.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/records.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/records.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,36 @@
+from twisted.python.filepath import FilePath
+
+class DirectoryRecord(object):
+    def __init__(self, uid, password, commonName, email, guid):
+        self.uid = uid
+        self.password = password
+        self.commonName = commonName
+        self.email = email
+        self.guid = guid
+
+    def __repr__(self):
+        return "Record(%s:%s %s %s %s)" % (self.uid, self.password, self.commonName, self.email, self.guid)
+
+# def generateRecords(
+#     count, uidPattern="user%d", passwordPattern="user%d",
+#     namePattern="User %d", emailPattern="user%d at example.com",
+#     guidPattern="user%d"
+# ):
+#     for i in xrange(count):
+#         i += 1
+#         uid = uidPattern % (i,)
+#         password = passwordPattern % (i,)
+#         name = namePattern % (i,)
+#         email = emailPattern % (i,)
+#         guid = guidPattern % (i,)
+#         yield DirectoryRecord(uid, password, name, email, guid)
+
+def recordsFromCSVFile(path):
+    if path:
+        pathObj = FilePath(path)
+    else:
+        pathObj = FilePath(__file__).sibling("accounts.csv")
+    return [
+        DirectoryRecord(*line.decode('utf-8').split(u','))
+        for line
+        in pathObj.getContent().splitlines()]

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/Profile
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/Profile	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/Profile	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,267 @@
+PROPFIND ./well-known/caldav				->	/principals/
+	current-user-principal
+	principal-URL
+	resourcetype
+
+PROPFIND /principals/						->
+	current-user-principal						/principals/__uids__/<uid>
+	principal-URL								----
+	resourcetype								collection
+
+OPTIONS /principals/__uids__/<uid>/
+
+PROPFIND /principals/__uids__/<uid>/
+	calendar-home-set							/calendars/__uids__/<uid>/
+	calendar-user-address-set					mailto:user#@example.com
+												urn:uuid:<uid>
+												urn:x-uid:<uid>
+	current-user-principal						/principals/__uids__/<uid>/
+	displayname									User #
+	dropbox-home-URL							/calendars/__uids__/<uid>/dropbox/
+	email-address-set							user#@example.com
+	notification-URL							/calendars/__uids__/<uid>/notification/
+	principal-collection-set					/principals/
+	principal-URL								/principals/__uids__/<uid>/
+	resource-id									urn:x-uid:<uid>
+	schedule-inbox-URL							/calendars/__uids__/<uid>/inbox/
+	schedule-outbox-URL							/calendars/__uids__/<uid>/outbox/
+	supported-report-set						acl-principal-prop-set
+												principal-match
+												principal-property-search
+												expand-property
+												calendarserver-principal-search
+
+OPTIONS /principals/__uids__/<uid>
+
+REPORT /principals/							-> 
+	principal-search-property-set				displayname
+												email-address-set
+												calendar-user-address-set
+												calendar-user-type 
+
+PROPFIND /calendars/__uids__/<uid>/inbox/	->	
+	calendar-availability						???
+
+PROPFIND /calendars/__uids__/<uid>/
+Depth 1
+	add-member									
+	allowed-sharing-modes									
+	autoprovisioned									
+	bulk-requests									
+	calendar-alarm									
+	calendar-color									
+	calendar-description									
+	calendar-free-busy-set									
+	calendar-order									
+	calendar-timezone									
+	current-user-privilege-set					all/read/read-free-busy/write/write-properties/write-content/bind/unbind/unlock/read-acl/write-acl/read-current-user-privilege-set				
+	default-alarm-vevent-date									
+	default-alarm-vevent-datetime									
+	displayname									User #
+	getctag									
+	invite									
+	language-code									
+	location-code									
+	owner										/principals/__uids__/<uid>/
+	pre-publish-url									
+	publish-url									
+	push-transports									
+	pushkey										/CalDAV/localhost/<uid>/
+	quota-available-bytes						104857600
+	quota-used-bytes							0
+	refreshrate									
+	resource-id									
+	resourcetype								collection	
+	schedule-calendar-transp									
+	schedule-default-calendar-URL									
+	source									
+	subscribed-strip-alarms									
+	subscribed-strip-attachments									
+	subscribed-strip-todos									
+	supported-calendar-component-set			VEVENT/VTODO						
+	supported-calendar-component-sets									
+	supported-report-set						acl-principal-prop-set/principal-match/principal-property-search/expand-property/calendarserver-principal-search/calendar-query/calendar-multiget/free-busy-query/addressbook-query/addressbook-multiget/sync-collection			
+	sync-token									data:,36_58/<hex>
+ 	** and more **
+
+PROPPATCH /calendars/__uids__/<uid>/		->		default-alarm-vevent-date
+PROPPATCH /calendars/__uids__/<uid>/		->		default-alarm-vevent-datetime
+
+PROPPATCH /calendars/__uids__/<uid>/calendar/	->		calendar-order
+PROPPATCH /calendars/__uids__/<uid>/calendar/	->		displayname
+PROPPATCH /calendars/__uids__/<uid>/calendar/	->		calendar-color
+PROPPATCH /calendars/__uids__/<uid>/calendar/	->		calendar-order
+PROPPATCH /calendars/__uids__/<uid>/calendar/	->		calendar-timezone
+
+PROPPATCH /calendars/__uids__/<uid>/tasks/	->		calendar-order
+PROPPATCH /calendars/__uids__/<uid>/tasks/	->		displayname
+PROPPATCH /calendars/__uids__/<uid>/tasks/	->		calendar-color
+PROPPATCH /calendars/__uids__/<uid>/tasks/	->		calendar-order
+PROPPATCH /calendars/__uids__/<uid>/tasks/	->		calendar-timezone
+
+PROPFIND /calendars/__uids__/<uid>/calendar/->
+	getctag										37_63
+	sync-token									data:,37_63/<hex>
+
+REPORT /calendars/__uids__/<uid>/calendar/ 	->
+	getcontenttype
+	getetag
+REPORT /calendar/__uids__/<uid>/calendar/
+	getcontenttype
+	getetag
+
+PROPFIND /calendars/__uids__/<uid>/			->
+	checksum-versions							???
+
+PROPFIND /calendars/__uids__/<uid>/calendar/	->
+	getctag										
+	sync-token										
+PROPFIND /calendars/__uids__/<uid>/calendar/
+	getcontenttype								httpd/unix-directory
+	getetag										"<hex>"
+
+PROPFIND /calendars/__uids__/<uid>/			-> (again?) 
+	checksum-versions
+
+PROPFIND /calendars/__uids__/<uid>/tasks/	->
+	getctag
+	sync-token
+PROPFIND /calendars/__uids__/<uid>/tasks/	->
+	getcontenttype
+	getetag
+
+PROPFIND /calendars/__uids__/<uid>/inbox/	->
+	getctag
+	sync-token
+PROPFIND /calendars/__uids__/<uid>/inbox/	->
+	getcontenttype
+	getetag
+
+PROPFIND /calendars/__uids__/<uid>/tasks/	->
+	getctag
+	sync-token
+PROPFIND /calendars/__uids__/<uid>/tasks/	->
+	getcontenttype
+	getetag
+
+PROPFIND /calendars/__uids__/<uid>/notification/	->
+	getctag
+	sync-token
+PROPFIND /calendars/__uids__/<uid>/notification/	->
+	notificationtype
+	getetag
+
+REPORT /principals/__uids__/<uid>/
+	calendar-proxy-write-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+	calendar-proxy-read-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+
+REPORT /calendars/__uids__/<uid>/
+	sync-collection
+		sync-token
+		sync-level
+		*lots of properties*
+
+PROPFIND /calendars/__uids__/<uid>/inbox/
+	getctag
+	sync-token
+
+PROPFIND /principals/__uids__/<uid>/
+	calendar-proxy-write-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+	calendar-proxy-read-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+
+----------------------------------------------------------------
+Deep Refresh (CMD + SHIFT + R)
+
+PROPFIND /principals/__uids__/<uid>/
+	<B:calendar-home-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:calendar-user-address-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:current-user-principal/>
+    <A:displayname/>
+    <C:dropbox-home-URL xmlns:C="http://calendarserver.org/ns/"/>
+    <C:email-address-set xmlns:C="http://calendarserver.org/ns/"/>
+    <C:notification-URL xmlns:C="http://calendarserver.org/ns/"/>
+    <A:principal-collection-set/>
+    <A:principal-URL/>
+    <A:resource-id/>
+    <B:schedule-inbox-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:schedule-outbox-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:supported-report-set/>
+
+OPTIONS /principals/__uids__/10000000-0000-0000-0000-000000000001/
+
+REPORT /principals/
+	principal-search-property-set
+
+PROPFIND /calendars/__uids__/10000000-0000-0000-0000-000000000001/inbox/
+	calendar-availability
+
+PROPFIND /calendars/__uids__/10000000-0000-0000-0000-000000000001/
+Depth 1
+	<A:add-member/>
+    <C:allowed-sharing-modes xmlns:C="http://calendarserver.org/ns/"/>
+    <D:autoprovisioned xmlns:D="http://apple.com/ns/ical/"/>
+    <E:bulk-requests xmlns:E="http://me.com/_namespace/"/>
+    <B:calendar-alarm xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <D:calendar-color xmlns:D="http://apple.com/ns/ical/"/>
+    <B:calendar-description xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:calendar-free-busy-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <D:calendar-order xmlns:D="http://apple.com/ns/ical/"/>
+    <B:calendar-timezone xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:current-user-privilege-set/>
+    <B:default-alarm-vevent-date xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:default-alarm-vevent-datetime xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:displayname/>
+    <C:getctag xmlns:C="http://calendarserver.org/ns/"/>
+    <C:invite xmlns:C="http://calendarserver.org/ns/"/>
+    <D:language-code xmlns:D="http://apple.com/ns/ical/"/>
+    <D:location-code xmlns:D="http://apple.com/ns/ical/"/>
+    <A:owner/>
+    <C:pre-publish-url xmlns:C="http://calendarserver.org/ns/"/>
+    <C:publish-url xmlns:C="http://calendarserver.org/ns/"/>
+    <C:push-transports xmlns:C="http://calendarserver.org/ns/"/>
+    <C:pushkey xmlns:C="http://calendarserver.org/ns/"/>
+    <A:quota-available-bytes/>
+    <A:quota-used-bytes/>
+    <D:refreshrate xmlns:D="http://apple.com/ns/ical/"/>
+    <A:resource-id/>
+    <A:resourcetype/>
+    <B:schedule-calendar-transp xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:schedule-default-calendar-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <C:source xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-alarms xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-attachments xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-todos xmlns:C="http://calendarserver.org/ns/"/>
+    <B:supported-calendar-component-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:supported-calendar-component-sets xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:supported-report-set/>
+    <A:sync-token/>
+
+PROPFIND on calendar/tasks/inbox/notifications as before
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+									
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/StartupProfile
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/StartupProfile	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/StartupProfile	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,71 @@
+PROPFIND  ./well-known/caldav					- startup_well_known_propfind
+
+PROPFIND  /principals/							- startup_principal_initial_propfind
+
+PROPFIND  /principals/__uids__/<uid>/			- startup_principal_propfind
+
+REPORT    /principals/							- startup_principals_report
+
+PROPFIND  /calendars/__uids__/<uid>/inbox/		- ???
+	calendar-availability						
+
+PROPFIND  /calendars/__uids__/<uid>/			- poll_calendar_home_depth1_propfind
+
+PROPPATCH /calendars/__uids__/<uid>/			- startup_calendarhome_default_alarm_date_proppatch
+PROPPATCH /calendars/__uids__/<uid>/			- startup_calendarhome_default_alarm_datetime_proppatch
+
+PROPPATCH /calendars/__uids__/<uid>/calendar/	- startup_calendar_order_proppatch
+PROPPATCH /calendars/__uids__/<uid>/calendar/	- startup_calendar_displayname_proppatch
+PROPPATCH /calendars/__uids__/<uid>/calendar/	- startup_calendar_color_proppatch
+PROPPATCH /calendars/__uids__/<uid>/calendar/	- startup_calendar_timezone_proppatch
+
+PROPPATCH /calendars/__uids__/<uid>/tasks/		- startup_calendar_order_proppatch
+PROPPATCH /calendars/__uids__/<uid>/tasks/		- startup_calendar_displayname_proppatch
+PROPPATCH /calendars/__uids__/<uid>/tasks/		- startup_calendar_color_proppatch
+PROPPATCH /calendars/__uids__/<uid>/tasks/		- startup_calendar_timezone_proppatch
+
+PROPFIND  /calendars/__uids__/<uid>/calendar/	- poll_calendar_propfind
+
+REPORT   /calendars/__uids__/<uid>/calendar/ 	- startup_query_events_depth1_report.request
+
+PROPFIND  /calendars/__uids__/<uid>/calendar/	- poll_calendar_propfind
+PROPFIND  /calendars/__uids__/<uid>/calendar/	- poll_calendar_depth1_propfind
+
+PROPFIND  /calendars/__uids__/<uid>/tasks/		- poll_calendar_propfind
+PROPFIND  /calendars/__uids__/<uid>/tasks/		- poll_calendar_depth1_propfind
+PROPFIND  /calendars/__uids__/<uid>/inbox/		- poll_calendar_propfind
+PROPFIND  /calendars/__uids__/<uid>/inbox/		- poll_calendar_depth1_propfind
+PROPFIND  /calendars/__uids__/<uid>/tasks/		- poll_calendar_propfind
+PROPFIND  /calendars/__uids__/<uid>/tasks/		- poll_calendar_depth1_propfind
+PROPFIND  /calendars/__uids__/<uid>/notification/	- poll_calendar_propfind
+PROPFIND  /calendars/__uids__/<uid>/notification/	- poll_notification_depth1_propfind
+
+REPORT    /principals/__uids__/<uid>/
+	calendar-proxy-write-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+	calendar-proxy-read-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+
+REPORT    /calendars/__uids__/<uid>/
+	sync-collection
+		sync-token
+		sync-level
+		*lots of properties*
+
+PROPFIND  /calendars/__uids__/<uid>/inbox/
+	getctag
+	sync-token
+
+PROPFIND  /principals/__uids__/<uid>/
+	calendar-proxy-write-for
+		calendar-user-address-set
+		email-address-set
+		displayname
+	calendar-proxy-read-for
+		calendar-user-address-set
+		email-address-set
+		displayname

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_depth1_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_depth1_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_depth1_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <A:getcontenttype/>
+    <A:getetag/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendar_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <C:getctag xmlns:C="http://calendarserver.org/ns/"/>
+    <A:sync-token/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendarhome_depth1_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendarhome_depth1_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_calendarhome_depth1_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <A:add-member/>
+    <C:allowed-sharing-modes xmlns:C="http://calendarserver.org/ns/"/>
+    <D:autoprovisioned xmlns:D="http://apple.com/ns/ical/"/>
+    <E:bulk-requests xmlns:E="http://me.com/_namespace/"/>
+    <B:calendar-alarm xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <D:calendar-color xmlns:D="http://apple.com/ns/ical/"/>
+    <B:calendar-description xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:calendar-free-busy-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <D:calendar-order xmlns:D="http://apple.com/ns/ical/"/>
+    <B:calendar-timezone xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:current-user-privilege-set/>
+    <B:default-alarm-vevent-date xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:default-alarm-vevent-datetime xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:displayname/>
+    <C:getctag xmlns:C="http://calendarserver.org/ns/"/>
+    <C:invite xmlns:C="http://calendarserver.org/ns/"/>
+    <D:language-code xmlns:D="http://apple.com/ns/ical/"/>
+    <D:location-code xmlns:D="http://apple.com/ns/ical/"/>
+    <A:owner/>
+    <C:pre-publish-url xmlns:C="http://calendarserver.org/ns/"/>
+    <C:publish-url xmlns:C="http://calendarserver.org/ns/"/>
+    <C:push-transports xmlns:C="http://calendarserver.org/ns/"/>
+    <C:pushkey xmlns:C="http://calendarserver.org/ns/"/>
+    <A:quota-available-bytes/>
+    <A:quota-used-bytes/>
+    <D:refreshrate xmlns:D="http://apple.com/ns/ical/"/>
+    <A:resource-id/>
+    <A:resourcetype/>
+    <B:schedule-calendar-transp xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:schedule-default-calendar-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <C:source xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-alarms xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-attachments xmlns:C="http://calendarserver.org/ns/"/>
+    <C:subscribed-strip-todos xmlns:C="http://calendarserver.org/ns/"/>
+    <B:supported-calendar-component-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:supported-calendar-component-sets xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:supported-report-set/>
+    <A:sync-token/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_notification_depth1_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_notification_depth1_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/poll_notification_depth1_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <A:getetag/>
+    <C:notificationtype xmlns:C="http://calendarserver.org/ns/"/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/post_freebusy.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/post_freebusy.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/post_freebusy.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VFREEBUSY
+UID:4288F0F3-5C5B-4DF4-9AD8-B1E5FE3F5B97
+DTSTART:20150804T211500Z
+DTEND:20150804T231500Z
+ATTENDEE:urn:uuid:30000000-0000-0000-0000-000000000005
+DTSTAMP:20150727T203410Z
+ORGANIZER:mailto:user01 at example.com
+END:VFREEBUSY
+END:VCALENDAR
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/principal_search_report.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/principal_search_report.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/principal_search_report.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<C:calendarserver-principal-search xmlns:C="http://calendarserver.org/ns/" context="{context}">
+  {searchTokens}
+  <A:prop xmlns:A="DAV:">
+    <C:email-address-set/>
+    <B:calendar-user-type xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:calendar-user-address-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:principal-URL/>
+    <C:last-name/>
+    <C:record-type/>
+    <A:displayname/>
+    <C:first-name/>
+  </A:prop>
+</C:calendarserver-principal-search>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/report_principal_search.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/report_principal_search.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/report_principal_search.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<C:calendarserver-principal-search xmlns:C="http://calendarserver.org/ns/" context="attendee">
+  <C:search-token>%(search)s</C:search-token>
+  <A:prop xmlns:A="DAV:">
+    <B:calendar-user-type xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <C:email-address-set/>
+    <A:displayname/>
+    <C:first-name/>
+    <C:last-name/>
+    <A:principal-URL/>
+    <C:record-type/>
+    <B:calendar-user-address-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+  </A:prop>
+</C:calendarserver-principal-search>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_color_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_color_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_color_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><D:calendar-color xmlns:D="http://apple.com/ns/ical/" symbolic-color="orange">#FD8208FF</D:calendar-color></A:prop></A:set></A:propertyupdate>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_description_proppatch.request.xml
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_description_proppatch.request.xml	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_description_proppatch.request.xml	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:calendar-description xmlns:B="urn:ietf:params:xml:ns:caldav">some description</B:calendar-description></A:prop></A:set></A:propertyupdate>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_displayname_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_displayname_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_displayname_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><A:displayname>calendar</A:displayname></A:prop></A:set></A:propertyupdate>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_order_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_order_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_order_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><D:calendar-order xmlns:D="http://apple.com/ns/ical/">1</D:calendar-order></A:prop></A:set></A:propertyupdate>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_timezone_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_timezone_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_timezone_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:calendar-timezone xmlns:B="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR&#13;
+VERSION:2.0&#13;
+PRODID:-//Apple Inc.//Mac OS X 10.11//EN&#13;
+CALSCALE:GREGORIAN&#13;
+BEGIN:VTIMEZONE&#13;
+TZID:America/Los_Angeles&#13;
+BEGIN:DAYLIGHT&#13;
+TZOFFSETFROM:-0800&#13;
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU&#13;
+DTSTART:20070311T020000&#13;
+TZNAME:PDT&#13;
+TZOFFSETTO:-0700&#13;
+END:DAYLIGHT&#13;
+BEGIN:STANDARD&#13;
+TZOFFSETFROM:-0700&#13;
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU&#13;
+DTSTART:20071104T020000&#13;
+TZNAME:PST&#13;
+TZOFFSETTO:-0800&#13;
+END:STANDARD&#13;
+END:VTIMEZONE&#13;
+END:VCALENDAR&#13;
+</B:calendar-timezone></A:prop></A:set></A:propertyupdate>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_transparent_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_transparent_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendar_transparent_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:schedule-calendar-transp xmlns:B="urn:ietf:params:xml:ns:caldav"><B:transparent/></B:schedule-calendar-transp></A:prop></A:set></A:propertyupdate>
+
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:schedule-calendar-transp xmlns:B="urn:ietf:params:xml:ns:caldav"><B:opaque/></B:schedule-calendar-transp></A:prop></A:set></A:propertyupdate>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_date_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_date_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_date_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:default-alarm-vevent-date xmlns:B="urn:ietf:params:xml:ns:caldav">BEGIN:VALARM&#13;
+X-WR-ALARMUID:49F29226-D2D7-4464-AE22-0147EDEFB2B4&#13;
+UID:49F29226-D2D7-4464-AE22-0147EDEFB2B4&#13;
+TRIGGER:-PT15H&#13;
+ATTACH;VALUE=URI:Basso&#13;
+ACTION:AUDIO&#13;
+END:VALARM&#13;
+</B:default-alarm-vevent-date></A:prop></A:set></A:propertyupdate>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_datetime_proppatch.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_datetime_proppatch.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_calendarhome_default_alarm_datetime_proppatch.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propertyupdate xmlns:A="DAV:"><A:set><A:prop><B:default-alarm-vevent-datetime xmlns:B="urn:ietf:params:xml:ns:caldav">BEGIN:VALARM&#13;
+X-WR-ALARMUID:4AD03A33-54A6-42BE-A157-47273DD60803&#13;
+UID:4AD03A33-54A6-42BE-A157-47273DD60803&#13;
+TRIGGER;VALUE=DATE-TIME:19760401T005545Z&#13;
+ACTION:NONE&#13;
+END:VALARM&#13;
+</B:default-alarm-vevent-datetime></A:prop></A:set></A:propertyupdate>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_create_calendar.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_create_calendar.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_create_calendar.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
+  <A:set xmlns:A="DAV:">
+    <A:prop>
+      <D:calendar-order xmlns:D="http://apple.com/ns/ical/">{order}</D:calendar-order>
+      <B:supported-calendar-component-set>
+        <B:comp name="{component_type}"/>
+      </B:supported-calendar-component-set>
+      <D:calendar-color xmlns:D="http://apple.com/ns/ical/" symbolic-color="custom">#{color}</D:calendar-color>
+      <B:calendar-timezone>BEGIN:VCALENDAR&#13;
+VERSION:2.0&#13;
+PRODID:-//Apple Inc.//Mac OS X 10.11//EN&#13;
+CALSCALE:GREGORIAN&#13;
+BEGIN:VTIMEZONE&#13;
+TZID:America/Los_Angeles&#13;
+BEGIN:DAYLIGHT&#13;
+TZOFFSETFROM:-0800&#13;
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU&#13;
+DTSTART:20070311T020000&#13;
+TZNAME:PDT&#13;
+TZOFFSETTO:-0700&#13;
+END:DAYLIGHT&#13;
+BEGIN:STANDARD&#13;
+TZOFFSETFROM:-0700&#13;
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU&#13;
+DTSTART:20071104T020000&#13;
+TZNAME:PST&#13;
+TZOFFSETTO:-0800&#13;
+END:STANDARD&#13;
+END:VTIMEZONE&#13;
+END:VCALENDAR&#13;
+</B:calendar-timezone>
+      <A:displayname>{name}</A:displayname>
+      <B:schedule-calendar-transp>
+        <B:opaque/>
+      </B:schedule-calendar-transp>
+    </A:prop>
+  </A:set>
+</B:mkcalendar>
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_delegate_principal_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_delegate_principal_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_delegate_principal_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <B:allowed-calendar-component-set xmlns:B="http://calendarserver.org/ns/"/>
+    <C:calendar-home-set xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+    <C:calendar-user-address-set xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+    <A:current-user-principal/>
+    <A:displayname/>
+    <B:dropbox-home-URL xmlns:B="http://calendarserver.org/ns/"/>
+    <B:email-address-set xmlns:B="http://calendarserver.org/ns/"/>
+    <B:notification-URL xmlns:B="http://calendarserver.org/ns/"/>
+    <A:principal-collection-set/>
+    <A:principal-URL/>
+    <A:resource-id/>
+    <C:schedule-inbox-URL xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+    <C:schedule-outbox-URL xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+    <A:supported-report-set/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_expand.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_expand.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_expand.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:expand-property xmlns:A="DAV:">
+  <A:property name="calendar-proxy-read-for" namespace="http://calendarserver.org/ns/">
+    <A:property name="calendar-user-address-set" namespace="urn:ietf:params:xml:ns:caldav"/>
+    <A:property name="email-address-set" namespace="http://calendarserver.org/ns/"/>
+    <A:property name="displayname" namespace="DAV:"/>
+  </A:property>
+  <A:property name="calendar-proxy-write-for" namespace="http://calendarserver.org/ns/">
+    <A:property name="calendar-user-address-set" namespace="urn:ietf:params:xml:ns:caldav"/>
+    <A:property name="email-address-set" namespace="http://calendarserver.org/ns/"/>
+    <A:property name="displayname" namespace="DAV:"/>
+  </A:property>
+</A:expand-property>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_initial_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_initial_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_initial_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <A:current-user-principal/>
+    <A:principal-URL/>
+    <A:resourcetype/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principal_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <B:calendar-home-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:calendar-user-address-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:current-user-principal/>
+    <A:displayname/>
+    <C:dropbox-home-URL xmlns:C="http://calendarserver.org/ns/"/>
+    <C:email-address-set xmlns:C="http://calendarserver.org/ns/"/>
+    <C:notification-URL xmlns:C="http://calendarserver.org/ns/"/>
+    <A:principal-collection-set/>
+    <A:principal-URL/>
+    <A:resource-id/>
+    <B:schedule-inbox-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <B:schedule-outbox-URL xmlns:B="urn:ietf:params:xml:ns:caldav"/>
+    <A:supported-report-set/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principals_report.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principals_report.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_principals_report.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:principal-search-property-set xmlns:A="DAV:"/>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_query_events_depth1_report.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_query_events_depth1_report.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_query_events_depth1_report.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<B:calendar-query xmlns:B="urn:ietf:params:xml:ns:caldav">
+  <A:prop xmlns:A="DAV:">
+    <A:getetag/>
+    <A:getcontenttype/>
+  </A:prop>
+  <B:filter>
+    <B:comp-filter name="VCALENDAR">
+      <B:comp-filter name="VEVENT">
+        <B:time-range start="20150630T010101Z" end="20150721T010101Z"/>
+      </B:comp-filter>
+    </B:comp-filter>
+  </B:filter>
+</B:calendar-query>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_well_known_propfind.request
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_well_known_propfind.request	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/request-data/OS_X_10_11/startup_well_known_propfind.request	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<A:propfind xmlns:A="DAV:">
+  <A:prop>
+    <A:current-user-principal/>
+    <A:principal-URL/>
+    <A:resourcetype/>
+  </A:prop>
+</A:propfind>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/requester.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/requester.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/requester.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,307 @@
+from caldavclientlibrary.protocol.webdav.propfindparser import PropFindParser
+
+from contrib.performance.httpauth import AuthHandlerAgent
+from contrib.performance.httpclient import StringProducer, readBody
+
+from twisted.web.http import OK, MULTI_STATUS, CREATED, NO_CONTENT
+from twisted.web.http_headers import Headers
+from twisted.web.client import Agent, ContentDecoderAgent, GzipDecoder, \
+    _DeprecatedToCurrentPolicyForHTTPS
+
+from twisted.python.log import msg
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.ssl import ClientContextFactory
+
+"""
+TODO
+Finish the comment table
+THink about better ways to do default headers
+Try to log messages in a more intelligent way
+
+"""
+
+class Requester(object):
+    """
+    Utility to create requests on behalf of a client. Public methods are:
+    method     url     body     headers     status     method_label
+    ------------------------------------------------------------------------
+    GET        req     ---
+    POST       req     req
+    PUT        req     req*
+    DELETE     req     ---
+    PROPFIND   req
+    PROPPATCH  req
+    REPORT     req
+    MKCALENDAR req
+
+    req: required
+    opt: optional
+    ---: disallowed
+    All of these rely on a private method _request
+
+    """
+
+    def __init__(
+        self,
+        root,
+        headers,
+        title,
+        uid,
+        client_id,
+        auth,
+        reactor
+    ):
+        self._root = root
+        self._headers = headers
+        self._title = title
+        self._uid = uid
+        self._client_id = client_id
+
+        self._reactor = reactor
+
+        # The server might use gzip encoding
+        agent = Agent(
+            self._reactor,
+            contextFactory=_DeprecatedToCurrentPolicyForHTTPS(WebClientContextFactory()),
+        )
+        agent = ContentDecoderAgent(agent, [("gzip", GzipDecoder)])
+        self._agent = AuthHandlerAgent(agent, auth)
+
+    def _addDefaultHeaders(self, headers):
+        """
+        Add the clients default set of headers to ones being used in a request.
+        Default is to add User-Agent, sub-classes should override to add other
+        client specific things, Accept etc.
+        """
+        for k, v in self._headers.iteritems():
+            headers.setRawHeaders(k, v)
+
+    @inlineCallbacks
+    def _request(self, method, url, expectedResponseCodes, headers=None, body=None, method_label=None):
+        """
+        Execute a request and check against the expected response codes.
+        """
+        if type(expectedResponseCodes) is int:
+            expectedResponseCodes = (expectedResponseCodes,)
+        if not method_label:
+            method_label = method
+        if headers is None:
+            headers = Headers({})
+        self._addDefaultHeaders(headers)
+        url = self._root + url.encode('utf-8')
+
+        msg(
+            type="request",
+            method=method_label,
+            url=url,
+            user=self._uid,
+            client_type=self._title,
+            client_id=self._client_id,
+        )
+
+
+        before = self._reactor.seconds()
+        response = yield self._agent.request(method, url, headers, StringProducer(body) if body else None)
+
+        # XXX This is time to receive response headers, not time
+        # to receive full response.  Should measure the latter, if
+        # not both.
+        after = self._reactor.seconds()
+
+        success = response.code in expectedResponseCodes
+
+        msg(
+            type="response",
+            success=success,
+            method=method_label,
+            headers=headers,
+            body=body,
+            code=response.code,
+            user=self._uid,
+            client_type=self._title,
+            client_id=self._client_id,
+            duration=(after - before),
+            url=url,
+        )
+
+        if success:
+            returnValue(response)
+
+        raise IncorrectResponseCode(expectedResponseCodes, response)
+
+
+    @inlineCallbacks
+    def get(self, url, method_label=None):
+        response = yield self._request(
+            'GET',
+            url,
+            (OK,),
+            method_label=method_label
+        )
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def post(self, url, body, headers=None, method_label=None):
+        response = yield self._request(
+            'POST',
+            url,
+            (OK, CREATED, MULTI_STATUS),
+            headers=headers,
+            body=body,
+            method_label=method_label
+        )
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def put(self, expectedResponseCodes, url, component, headers=None, method_label=None):
+        response = yield self._request(
+            'PUT',
+            url,
+            expectedResponseCodes,
+            headers=headers,
+            body=component.getTextWithTimezones(includeTimezones=True),
+            method_label=method_label
+        )
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def delete(self, url, method_label=None):
+        response = yield self._request(
+            'DELETE',
+            url,
+            (NO_CONTENT,),
+            method_label=method_label
+        )
+        returnValue(response)
+
+
+    def _parseMultiStatus(self, response, otherTokens=False):
+        """
+        Parse a <multistatus> - might need to return other top-level elements
+        in the response - e.g. DAV:sync-token
+        I{PROPFIND} request for the principal URL.
+
+        @type response: C{str}
+        @rtype: C{cls}
+        """
+        parser = PropFindParser()
+        parser.parseData(response)
+        if otherTokens:
+            return (parser.getResults(), parser.getOthers(),)
+        else:
+            return parser.getResults()
+
+    @inlineCallbacks
+    def propfind(self, url, body, depth='0', allowedStatus=(MULTI_STATUS,), method_label=None):
+        """
+        Issue a PROPFIND on the chosen URL
+        """
+        hdrs = Headers({'content-type': ['text/xml']})
+        if depth is not None:
+            hdrs.addRawHeader('depth', depth)
+        response = yield self._request(
+            'PROPFIND',
+            url,
+            allowedStatus,
+            headers=hdrs,
+            body=body,
+            method_label=method_label,
+        )
+
+        body = yield readBody(response)
+        result = self._parseMultiStatus(body) if response.code == MULTI_STATUS else None
+
+        returnValue((response, result,))
+
+    @inlineCallbacks
+    def proppatch(self, url, body, method_label=None):
+        """
+        Issue a PROPPATCH on the chosen URL
+        """
+        hdrs = Headers({'content-type': ['text/xml']})
+        response = yield self._request(
+            'PROPPATCH',
+            url,
+            (OK, MULTI_STATUS,),
+            headers=hdrs,
+            body=body,
+            method_label=method_label,
+        )
+        if response.code == MULTI_STATUS:
+            body = yield readBody(response)
+            result = self._parseMultiStatus(body)
+            returnValue(result)
+        else:
+            returnValue(None)
+
+    @inlineCallbacks
+    def report(self, url, body, depth='0', allowedStatus=(MULTI_STATUS,), otherTokens=False, method_label=None):
+        """
+        Issue a REPORT on the chosen URL
+        """
+        hdrs = Headers({'content-type': ['text/xml']})
+        if depth is not None:
+            hdrs.addRawHeader('depth', depth)
+        response = yield self._request(
+            'REPORT',
+            url,
+            allowedStatus,
+            headers=hdrs,
+            body=body,
+            method_label=method_label,
+        )
+
+        body = yield readBody(response)
+        result = self._parseMultiStatus(body, otherTokens) if response.code == MULTI_STATUS else None
+
+        returnValue(result)
+
+    @inlineCallbacks
+    def mkcalendar(self, url, body, method_label=None):
+        """
+        Issue a MKCALENDAR on the chosen URL with the given body
+        url: an href like /calendars/__uids__/<user-uid>/<calendar-uid>/
+        body: the XML body of the request
+        """
+        headers = Headers({'content-type': ['text/xml']})
+        response = yield self._request(
+            'MKCALENDAR',
+            url,
+            (CREATED,),
+            headers=headers,
+            body=body,
+            method_label=method_label
+        )
+        body = yield readBody(response)
+        returnValue(body)
+
+
+class IncorrectResponseCode(Exception):
+    """
+    Raised when a response has a code other than the one expected.
+
+    @ivar expected: The response codes which was expected.
+    @type expected: C{tuple} of C{int}
+
+    @ivar response: The response which was received
+    @type response: L{twisted.web.client.Response}
+    """
+    def __init__(self, expected, response):
+        self.expected = expected
+        self.response = response
+
+    def __repr__(self):
+        return "%s "
+
+
+class WebClientContextFactory(ClientContextFactory):
+    """
+    A web context factory which ignores the hostname and port and does no
+    certificate verification.
+    """
+    def getContext(self, hostname, port):
+        return ClientContextFactory.getContext(self)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/resources.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/resources.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/resources.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,214 @@
+from twistedcaldav.ical import Component
+
+from urlparse import urljoin
+
+from caldavclientlibrary.protocol.caldav.definitions import caldavxml
+
+from caldavclientlibrary.protocol.calendarserver.invite import AddInvitees, RemoveInvitee, InviteUser
+from caldavclientlibrary.protocol.webdav.proppatch import PropPatch
+
+import os
+
+from xml.etree import ElementTree
+
+def u2str(data):
+    return data.encode("utf-8") if type(data) is unicode else data
+
+
+class Event(object):
+    def __init__(self, serializeBasePath, url, etag, component=None):
+        self.serializeBasePath = serializeBasePath
+        self.url = url
+        self.etag = etag
+        self.scheduleTag = None
+        if component is not None:
+            self.component = component
+        self.uid = component.resourceUID() if component is not None else None
+
+    def getUID(self):
+        """
+        Return the UID of the calendar resource.
+        """
+        return self.uid
+
+
+    def serializePath(self):
+        if self.serializeBasePath:
+            calendar = os.path.join(self.serializeBasePath, self.url.split("/")[-2])
+            if not os.path.exists(calendar):
+                os.makedirs(calendar)
+            return os.path.join(calendar, self.url.split("/")[-1])
+        else:
+            return None
+
+
+    def serialize(self):
+        """
+        Create a dict of the data so we can serialize as JSON.
+        """
+
+        result = {}
+        for attr in ("url", "etag", "scheduleTag", "uid",):
+            result[attr] = getattr(self, attr)
+        return result
+
+
+    @staticmethod
+    def deserialize(serializeLocation, data):
+        """
+        Convert dict (deserialized from JSON) into an L{Event}.
+        """
+
+        event = Event(serializeLocation, None, None)
+        for attr in ("url", "etag", "scheduleTag", "uid",):
+            setattr(event, attr, u2str(data[attr]))
+        return event
+
+
+    @property
+    def component(self):
+        """
+        Data always read from disk - never cached in the object.
+        """
+        path = self.serializePath()
+        if path and os.path.exists(path):
+            f = open(path)
+            comp = Component.fromString(f.read())
+            f.close()
+            return comp
+        else:
+            return None
+
+
+    @component.setter
+    def component(self, component):
+        """
+        Data always written to disk - never cached on the object.
+        """
+        path = self.serializePath()
+        if path:
+            if component is None:
+                os.remove(path)
+            else:
+                f = open(path, "w")
+                f.write(str(component))
+                f.close()
+        self.uid = component.resourceUID() if component is not None else None
+
+
+    def removed(self):
+        """
+        Resource no longer exists on the server - remove associated data.
+        """
+        path = self.serializePath()
+        if path and os.path.exists(path):
+            os.remove(path)
+
+
+
+class Calendar(object):
+    def __init__(self, resourceType, componentTypes, name, url, changeToken):
+        self.resourceType = resourceType
+        self.componentTypes = componentTypes
+        self.name = name
+        self.url = url
+        self.changeToken = changeToken
+        self.events = {}
+        if self.name is None and self.url is not None:
+            self.name = self.url.rstrip("/").split("/")[-1]
+
+    def serialize(self):
+        """
+        Create a dict of the data so we can serialize as JSON.
+        """
+
+        result = {}
+        for attr in ("resourceType", "name", "url", "changeToken"):
+            result[attr] = getattr(self, attr)
+        result["componentTypes"] = list(sorted(self.componentTypes))
+        result["events"] = sorted(self.events.keys())
+        return result
+
+
+    @staticmethod
+    def deserialize(data, events):
+        """
+        Convert dict (deserialized from JSON) into an L{Calendar}.
+        """
+
+        calendar = Calendar(None, None, None, None, None)
+        for attr in ("resourceType", "name", "url", "changeToken"):
+            setattr(calendar, attr, u2str(data[attr]))
+        calendar.componentTypes = set(map(u2str, data["componentTypes"]))
+
+        for event in data["events"]:
+            url = urljoin(calendar.url, event)
+            if url in events:
+                calendar.events[event] = events[url]
+            else:
+                # Ughh - an event is missing - force changeToken to empty to trigger full resync
+                calendar.changeToken = ""
+        return calendar
+
+
+    @staticmethod
+    def addInviteeXML(uid, summary, readwrite=True):
+        return AddInvitees(None, '/', [uid], readwrite, summary=summary).request_data.text
+
+
+    @staticmethod
+    def removeInviteeXML(uid):
+        invitee = InviteUser()
+        # Usually an InviteUser is populated through .parseFromUser, but we only care about a uid
+        invitee.user_uid = uid
+        return RemoveInvitee(None, '/', invitee).request_data.text
+
+
+    @staticmethod
+    def _buildPropPatchXML(element):
+        """
+        Change the specified element on the calendar given by href.
+        """
+        return PropPatch(None, '/', [element]).request_data.text
+
+
+    # def setCalendarDisplayName(self, calendar, displayname):
+    #     self._calendars[calendar.url].displayname = displayname # Update the cached copy
+    #     qn = davxml.displayname
+    #     el = ElementTree.Element(qn)
+    #     el.text = displayname
+    #     yield self._property_update(el)
+
+    @staticmethod
+    def setCalendarDescriptionXML(calendar, description):
+        qn = caldavxml.calendar_description
+        el = ElementTree.Element(qn)
+        el.text = description
+        return Calendar._buildPropPatchXML(el)
+
+
+    @staticmethod
+    def setCalendarTransparencyXML(calendar, isTransparent):
+        qn = caldavxml.schedule_calendar_transp
+        el = ElementTree.Element(qn)
+        transp_qn = caldavxml.transparent if isTransparent else caldavxml.opaque
+        ElementTree.SubElement(el, transp_qn)
+        return Calendar._buildPropPatchXML(el)
+
+
+    @staticmethod
+    def setCalendarColorXML(calendar, color):
+        """ color is an RGBA string, e.g. "#FF0088FF" """
+        qn = ElementTree.QName('http://apple.com/ns/ical/', 'calendar-color')
+        el = ElementTree.Element(qn)
+        el.text = color
+        el.set('symbolic-color', 'custom')
+        return Calendar._buildPropPatchXML(el)
+
+
+    @staticmethod
+    def setCalendarOrder(self, calendar, order):
+        qn = ElementTree.QName('http://apple.com/ns/ical/', 'calendar-order')
+        el = ElementTree.Element(qn)
+        el.text = order
+        return Calendar._buildPropPatchXML(el)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sandbox.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sandbox.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sandbox.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,33 @@
+import numpy as np
+from scipy.optimize import curve_fit
+from scipy import stats
+
+
+values = [(0,16704),(1,36939),(2,13483),(3,6779),(4,4325),(5,2803),(6,2088),(7,1697),(8,1283),(9,976),(10,829),(11,623),(12,512),(13,449),(14,368),(15,300),(16,257),(17,236),(18,238),(19,181),(20,174),(21,171),(22,145),(23,122),(24,104),(25,105),(26,85),(27,66),(28,56),(29,58),(30,58),(31,64),(32,81),(33,37),(34,45),(35,37),(36,33),(37,35),(38,36),(39,17),(40,22),(41,19),(42,16),(43,32),(44,20),(45,15),(46,18),(47,11),(48,8),(49,24),(50,10),(51,22),(52,12),(53,12),(54,12),(55,16),(56,17),(57,12),(58,9),(59,6),(60,2),(61,7),(62,9),(63,5),(64,9),(65,15),(66,10),(67,4),(68,3),(69,4),(70,5),(71,13),(72,5),(73,4),(74,2),(75,5),(76,7),(77,6),(78,4),(79,3),(80,2),(81,2),(82,1),(84,3),(85,2),(86,1),(90,4),(91,1),(92,3),(93,1),(94,3),(95,1),(96,4),(97,2),(98,1),(100,2),(101,1),(104,1),(105,3),(106,1),(108,3),(109,1),(110,1),(113,1),(114,2),(115,2),(116,10),(117,1),(118,1),(119,1)]
+
+def toDistribution(values):
+    """
+    Converts an array of (x,y) pairs to a distribution object
+    """
+    xdata, ydata = map(np.array(zip(*values)))
+
+    popt, pcov = curve_fit(normalPDF, xdata, ydata)
+    return popt, pcov
+
+
+def normalPDF(xdata, mu, sigma):
+    rv = stats.norm(mu, sigma)
+    return rv.pdf(xdata)
+
+def lognormPDF(xdata, mu, sigma):
+    """
+    If log(x) is normally distributed with mean mu and variance sigma**2
+    then x is log-normally distributed with shape parameter sigma and scale parameter exp(mu).
+    """
+    rv = stats.lognorm(sigma, 0, np.exp(mu))
+    return rv.pdf(xdata)
+
+line = np.linspace(1, 10, 100)
+realnorm = lognormPDF(line, 1, 3)
+
+popt, pcov = toDistribution(np.array(zip(line, realnorm)))

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/README.md
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/README.md	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/README.md	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,3 @@
+# Settings
+
+Documentation about how to use and manipulate the settings files go here
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/__init__.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/__init__.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2012-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/calendars-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/calendars-only.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/calendars-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+			<dict>
+				<!-- Here is a El Capitan iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_11</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.11</string>
+
+					<!-- Client can poll the calendar home at some interval. This is
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>5</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.11 can wait for notifications
+						about calendar home changes instead of polling for them periodically. If
+						this option is true, then look for the server advertisement for xmpp push
+						and use it if possible. Still fall back to polling if there is no xmpp push
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<true />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an calendar-creating profile, which will periodically create a new calendar -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.CalendarMaker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true />
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new calendar. -->
+							<key>interval</key>
+							<integer>15</integer>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically change something about the event. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.CalendarUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true />
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to update an existing calendar. -->
+							<key>interval</key>
+							<integer>5</integer>
+						</dict>
+					</dict>
+
+					<!-- This profile randomly shares calendars. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.CalendarSharer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false />
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to share an existing calendar. -->
+							<key>interval</key>
+							<integer>30</integer>
+						</dict>
+					</dict>
+
+					<!-- This profile randomly deletes calendars. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.CalendarDeleter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true />
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to delete an existing calendar. -->
+							<key>interval</key>
+							<integer>30</integer>
+						</dict>
+					</dict>
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients-old.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients-old.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients-old.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,527 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a OS X client simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.clients.OS_X_10_7</string>
+
+				<!-- Arguments to use to initialize the OS_X_10_7 instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.7</string>
+	
+					<!-- OS_X_10_7 can poll the calendar home at some interval. This is
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>30</integer>
+
+					<!-- If the server advertises xmpp push, OS_X_10_7 can wait for notifications
+						about calendar home changes instead of polling for them periodically. If
+						this option is true, then look for the server advertisement for xmpp push
+						and use it if possible. Still fall back to polling if there is no xmpp push
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+
+					<key>supportAmpPush</key>
+					<true/>
+					<key>ampPushHost</key>
+					<string>localhost</string>
+					<key>ampPushPort</key>
+					<integer>62311</integer>
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile invites some number of new attendees to new events. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the frequency at which new invitations will be sent out. -->
+							<key>sendInvitationDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.NormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mu gives the mean of the normal distribution (in seconds). -->
+									<key>mu</key>
+									<integer>60</integer>
+
+									<!-- and sigma gives its standard deviation. -->
+									<key>sigma</key>
+									<integer>5</integer>
+								</dict>
+							</dict>
+
+							<!-- Define the distribution of who will be invited to an event.
+							
+								When inviteeClumping is turned on each invitee is based on a sample of
+								users "close to" the organizer based on account index. If the clumping
+								is too "tight" for the requested number of attendees, then invites for
+								those larger numbers will simply fail (the sim will report that situation).
+								
+								When inviteeClumping is off invitees will be sampled across an entire
+								range of account indexes. In this case the distribution ought to be a
+								UniformIntegerDistribution with min=0 and max set to the number of accounts.
+							-->
+							<key>inviteeDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformIntegerDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- The minimum value (inclusive) of the uniform distribution. -->
+									<key>min</key>
+									<integer>0</integer>
+									<!-- The maximum value (exclusive) of the uniform distribution. -->
+									<key>max</key>
+									<integer>99</integer>
+								</dict>
+							</dict>
+
+							<key>inviteeClumping</key>
+							<true/>
+
+							<!-- Define the distribution of how many attendees will be invited to an event.
+							
+								LogNormal is the best fit to observed data.
+
+
+								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
+								mode should typically be 1, and mean whatever matches the user behavior.
+								Our typical mean is 6. 							
+							     -->
+							<key>inviteeCountDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>1</integer>
+									<!-- mean - average-->
+									<key>median</key>
+									<integer>6</integer>
+									<!-- maximum -->
+									<key>maximum</key>
+									<real>60</real>
+								</dict>
+							</dict>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile accepts invitations to events, handles cancels, and
+					     handles replies received. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Accepter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define how long to wait after seeing a new invitation before
+								accepting it.
+
+								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
+								(i.e., half of the user have accepted by that time).								
+							-->
+							<key>acceptDelayDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>300</integer>
+									<!-- median - 50% done-->
+									<key>median</key>
+									<integer>1800</integer>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- A task-creating profile, which will periodically create
+						new tasks at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Tasker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>300</integer>
+						</dict>
+					</dict>
+
+					<!-- A task-updating profile, which will periodically create
+						new tasks at a random time on a random calendar and then
+						update them in some way -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.TaskUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>60</integer>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a OS X client simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.clients.OS_X_10_11</string>
+
+				<!-- Arguments to use to initialize the OS_X_10_7 instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.11</string>
+
+					<!-- OS_X_10_7 can poll the calendar home at some interval. This is
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>30</integer>
+
+					<!-- If the server advertises AMP push, the client can wait for notifications about calendar home changes in addition to polling them periodically. If this option is true, look for the server advertisement for AMP push and use it if possible
+						about calendar home changes instead of polling for them periodically. If
+						this option is true, then look for the server advertisement for xmpp push
+						and use it if possible. Still fall back to polling if there is no xmpp push
+						advertised. -->
+					<key>supportAmpPush</key>
+					<true/>
+					<key>ampPushHost</key>
+					<string>localhost</string>
+					<key>ampPushPort</key>
+					<integer>62311</integer>
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>2</integer>
+						</dict>
+					</dict>
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.dist.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.dist.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.dist.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- This is a distributed orchestrator configuration; 'workers' is a list of
+							shell commands to run sub-processes.
+							-->
+		<key>workers</key>
+		<array>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+			<string>./bin/python contrib/performance/loadtest/ampsim.py</string>
+		</array>
+
+		<!-- Identify the server to be load tested. -->
+		<key>server</key>
+		<string>https://127.0.0.1:8443</string>
+
+		<!-- Configure Admin Web UI. -->
+		<key>webadmin</key>
+		<dict>
+			<key>enabled</key>
+			<true/>
+
+			<key>HTTPPort</key>
+			<integer>8080</integer>
+		</dict>
+
+		<!--  Define whether server supports stats socket. -->
+		<key>serverStats</key>
+		<dict>
+			<key>enabled</key>
+			<true/>
+			<key>Port</key>
+			<integer>8100</integer>
+		</dict>
+
+		<!--  Define whether client data should be re-used. It will always be saved to the specified path.-->
+		<key>clientDataSerialization</key>
+		<dict>
+			<key>UseOldData</key>
+			<true/>
+			<key>Path</key>
+			<string>/tmp/sim</string>
+		</dict>
+
+		<!-- Define the credentials of the clients which will be used to load test
+			the server. These credentials must already be valid on the server. -->
+		<key>accounts</key>
+		<dict>
+			<!-- The loader is the fully-qualified Python name of a callable which
+				returns a list of directory service records defining all of the client accounts
+				to use. contrib.performance.loadtest.sim.recordsFromCSVFile reads username,
+				password, mailto triples from a CSV file and returns them as a list of faked
+				directory service records. -->
+			<key>loader</key>
+			<string>contrib.performance.loadtest.sim.recordsFromCSVFile</string>
+
+			<!-- Keyword arguments may be passed to the loader. -->
+			<key>params</key>
+			<dict>
+				<!-- recordsFromCSVFile interprets the path relative to the config.plist,
+					to make it independent of the script's working directory while still allowing
+					a relative path. This isn't a great solution. -->
+				<key>path</key>
+				<string>contrib/performance/loadtest/accounts.csv</string>
+			</dict>
+		</dict>
+
+		<!-- Define how many clients will participate in the load test and how
+			they will show up. -->
+		<key>arrival</key>
+		<dict>
+
+			<!-- Specify a class which creates new clients and introduces them into
+				the test. contrib.performance.loadtest.population.SmoothRampUp introduces
+				groups of new clients at fixed intervals up to a maximum. The size of the
+				group, interval, and maximum are configured by the parameters below. The
+				total number of clients is groups * groupSize, which needs to be no larger
+				than the number of credentials created in the accounts section. -->
+			<key>factory</key>
+			<string>contrib.performance.loadtest.population.SmoothRampUp</string>
+
+			<key>params</key>
+			<dict>
+				<!-- groups gives the total number of groups of clients to introduce. -->
+				<key>groups</key>
+				<integer>99</integer>
+
+				<!-- groupSize is the number of clients in each group of clients. It's
+					really only a "smooth" ramp up if this is pretty small. -->
+				<key>groupSize</key>
+				<integer>1</integer>
+
+				<!-- Number of seconds between the introduction of each group. -->
+				<key>interval</key>
+				<integer>3</integer>
+
+				<!-- Number of clients each user is assigned to. -->
+				<!-- Set weight of clients to 1 if this is > 1. Number of clients must match this value if > 1. -->
+				<key>clientsPerUser</key>
+				<integer>1</integer>
+			</dict>
+
+		</dict>
+
+		<!-- Define some log observers to report on the load test. -->
+		<key>observers</key>
+		<array>
+			<!-- ReportStatistics generates an end-of-run summary of the HTTP requests
+				made, their timings, and their results. -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.ReportStatistics</string>
+				<key>params</key>
+				<dict>
+					<!-- The thresholds for each request type -->
+					<key>thresholdsPath</key>
+					<string>contrib/performance/loadtest/thresholds.json</string>
+
+					<!-- The benchmarks for overall QoS -->
+					<key>benchmarksPath</key>
+					<string>contrib/performance/loadtest/benchmarks.json</string>
+
+					<!-- The % of failures that constitute a failed test -->
+					<key>failCutoff</key>
+					<real>1.0</real>
+				</dict>
+			</dict>
+
+			<!-- RequestLogger generates a realtime log of all HTTP requests made
+				during the load test. -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.RequestLogger</string>
+				<key>params</key>
+				<dict>
+				</dict>
+			</dict>
+
+			<!-- OperationLogger generates an end-of-run summary of the gross operations
+				performed (logical operations which may span more than one HTTP request,
+				such as inviting an attendee to an event). -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.OperationLogger</string>
+				<key>params</key>
+				<dict>
+					<!-- The thresholds for each operation type -->
+					<key>thresholdsPath</key>
+					<string>contrib/performance/loadtest/thresholds.json</string>
+
+					<!-- The % of operations beyond the lag cut-off that constitute a failed test -->
+					<key>lagCutoff</key>
+					<real>1.0</real>
+
+					<!-- The % of failures that constitute a failed test -->
+					<key>failCutoff</key>
+					<real>1.0</real>
+				</dict>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/config.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Identify the server to be load tested. -->
+		<key>server</key>
+		<string>https://127.0.0.1:8443</string>
+
+		<!-- Configure Admin Web UI. -->
+		<key>webadmin</key>
+		<dict>
+			<key>enabled</key>
+			<true/>
+
+			<key>HTTPPort</key>
+			<integer>8080</integer>
+		</dict>
+
+		<!--  Define whether server supports stats socket. -->
+		<key>serverStats</key>
+		<dict>
+			<key>enabled</key>
+			<true/>
+			<key>Port</key>
+			<integer>8100</integer>
+		</dict>
+
+		<!--  Define whether client data should be re-used. It will always be saved to the specified path.-->
+		<key>clientDataSerialization</key>
+		<dict>
+			<key>UseOldData</key>
+			<true/>
+			<key>Path</key>
+			<string>/tmp/sim</string>
+		</dict>
+
+		<!-- Define the credentials of the clients which will be used to load test
+			the server. These credentials must already be valid on the server. -->
+		<key>accounts</key>
+		<dict>
+			<!-- The loader is the fully-qualified Python name of a callable which
+				returns a list of directory service records defining all of the client accounts
+				to use. contrib.performance.loadtest.sim.recordsFromCSVFile reads username,
+				password, mailto triples from a CSV file and returns them as a list of faked
+				directory service records. -->
+			<key>loader</key>
+			<string>contrib.performance.loadtest.records.recordsFromCSVFile</string>
+
+			<!-- Keyword arguments may be passed to the loader. -->
+			<key>params</key>
+			<dict>
+				<!-- recordsFromCSVFile interprets the path relative to the config.plist,
+					to make it independent of the script's working directory while still allowing
+					a relative path. This isn't a great solution. -->
+				<key>path</key>
+				<string>contrib/performance/loadtest/accounts.csv</string>
+			</dict>
+		</dict>
+
+		<!-- Define how many clients will participate in the load test and how
+			they will show up. -->
+		<key>arrival</key>
+		<dict>
+
+			<!-- Specify a class which creates new clients and introduces them into
+				the test. contrib.performance.loadtest.population.SmoothRampUp introduces
+				groups of new clients at fixed intervals up to a maximum. The size of the
+				group, interval, and maximum are configured by the parameters below. The
+				total number of clients is groups * groupSize, which needs to be no larger
+				than the number of credentials created in the accounts section. -->
+			<key>factory</key>
+			<string>contrib.performance.loadtest.population.SmoothRampUp</string>
+
+			<key>params</key>
+			<dict>
+				<!-- groups gives the total number of groups of clients to introduce. -->
+				<key>groups</key>
+				<integer>3</integer>
+
+				<!-- groupSize is the number of clients in each group of clients. It's
+					really only a "smooth" ramp up if this is pretty small. -->
+				<key>groupSize</key>
+				<integer>1</integer>
+
+				<!-- Number of seconds between the introduction of each group. -->
+				<key>interval</key>
+				<integer>15</integer>
+
+				<!-- Number of clients each user is assigned to. -->
+				<!-- Set weight of clients to 1 if this is > 1. Number of clients must match this value if > 1. -->
+				<key>clientsPerUser</key>
+				<integer>1</integer>
+			</dict>
+
+		</dict>
+
+		<!-- Define some log observers to report on the load test. -->
+		<key>observers</key>
+		<array>
+			<!-- ReportStatistics generates an end-of-run summary of the HTTP requests
+				made, their timings, and their results. -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.ReportStatistics</string>
+				<key>params</key>
+				<dict>
+					<!-- The thresholds for each request type -->
+					<key>thresholdsPath</key>
+					<string>contrib/performance/loadtest/thresholds.json</string>
+
+					<!-- The benchmarks for overall QoS -->
+					<key>benchmarksPath</key>
+					<string>contrib/performance/loadtest/benchmarks.json</string>
+
+					<!-- The % of failures that constitute a failed test -->
+					<key>failCutoff</key>
+					<real>1.0</real>
+				</dict>
+			</dict>
+
+			<!-- RequestLogger generates a realtime log of all HTTP requests made
+				during the load test. -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.RequestLogger</string>
+				<key>params</key>
+				<dict>
+				</dict>
+			</dict>
+
+			<!-- OperationLogger generates an end-of-run summary of the gross operations
+				performed (logical operations which may span more than one HTTP request,
+				such as inviting an attendee to an event). -->
+			<dict>
+				<key>type</key>
+				<string>contrib.performance.loadtest.logger.OperationLogger</string>
+				<key>params</key>
+				<dict>
+					<!-- The thresholds for each operation type -->
+					<key>thresholdsPath</key>
+					<string>contrib/performance/loadtest/thresholds.json</string>
+
+					<!-- The % of operations beyond the lag cut-off that constitute a failed test -->
+					<key>lagCutoff</key>
+					<real>1.0</real>
+
+					<!-- The % of failures that constitute a failed test -->
+					<key>failCutoff</key>
+					<real>1.0</real>
+				</dict>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/demo-clients.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/demo-clients.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/demo-clients.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<key>clients</key>
+		<array>
+			<dict>
+				<!-- Here is a OS X client simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.clients.OS_X_10_11</string>
+
+				<!-- Arguments to use to initialize the OS_X_10_7 instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.11</string>
+
+					<!-- OS_X_10_11 can poll the calendar home at some interval. This is
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>30</integer>
+
+					<!-- If the server advertises AMP push, the client can wait for notifications about calendar home changes in addition to polling them periodically. If this option is true, look for the server advertisement for AMP push and use it if possible. Still fall back to polling if there is no AMP push						advertised. -->
+					<key>supportAmpPush</key>
+					<true/>
+					<key>ampPushHost</key>
+					<string>localhost</string>
+					<key>ampPushPort</key>
+					<integer>62311</integer>
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>1</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Rescheduler</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+							<key>interval</key>
+							<integer>5</integer>
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/event-updates-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/event-updates-only.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/event-updates-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,543 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a Lion iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.7</string>
+
+					<!-- Client can poll the calendar home at some interval. This is 
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>300000</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
+						about calendar home changes instead of polling for them periodically. If 
+						this option is true, then look for the server advertisement for xmpp push 
+						and use it if possible. Still fall back to polling if there is no xmpp push 
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<false />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the 
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create 
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>20</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>5</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile invites some number of new attendees to new events. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the frequency at which new invitations will be sent out. -->
+							<key>sendInvitationDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.NormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mu gives the mean of the normal distribution (in seconds). -->
+									<key>mu</key>
+									<integer>10</integer>
+
+									<!-- and sigma gives its standard deviation. -->
+									<key>sigma</key>
+									<integer>5</integer>
+								</dict>
+							</dict>
+
+							<!-- Define the distribution of who will be invited to an event.
+							
+								When inviteeClumping is turned on each invitee is based on a sample of
+								users "close to" the organizer based on account index. If the clumping
+								is too "tight" for the requested number of attendees, then invites for
+								those larger numbers will simply fail (the sim will report that situation).
+								
+								When inviteeClumping is off invitees will be sampled across an entire
+								range of account indexes. In this case the distribution ought to be a
+								UniformIntegerDistribution with min=0 and max set to the number of accounts.
+							-->
+							<key>inviteeDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformIntegerDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- The minimum value (inclusive) of the uniform distribution. -->
+									<key>min</key>
+									<integer>0</integer>
+									<!-- The maximum value (exclusive) of the uniform distribution. -->
+									<key>max</key>
+									<integer>99</integer>
+								</dict>
+							</dict>
+
+							<key>inviteeClumping</key>
+							<true/>
+
+							<!-- Define the distribution of how many attendees will be invited to an event.
+							
+								LogNormal is the best fit to observed data.
+
+
+								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
+								mode should typically be 1, and mean whatever matches the user behavior.
+								Our typical mean is 6. 							
+							     -->
+							<key>inviteeCountDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>1</integer>
+									<!-- mean - average-->
+									<key>median</key>
+									<integer>6</integer>
+									<!-- maximum -->
+									<key>maximum</key>
+									<real>100</real>
+								</dict>
+							</dict>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile accepts invitations to events, handles cancels, and
+					     handles replies received. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Accepter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define how long to wait after seeing a new invitation before
+								accepting it.
+
+								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
+								(i.e., half of the user have accepted by that time).								
+							-->
+							<key>acceptDelayDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>300</integer>
+									<!-- median - 50% done-->
+									<key>median</key>
+									<integer>1800</integer>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- A task-creating profile, which will periodically create 
+						new tasks at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Tasker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how due times (DUE) for the randomly generated tasks 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>taskDueDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will 
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/events-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/events-only.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/events-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,280 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is an El Captian iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_11</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.11</string>
+
+					<!-- Client can poll the calendar home at some interval. This is 
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>30</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.11 can wait for notifications 
+						about calendar home changes instead of polling for them periodically. If 
+						this option is true, then look for the server advertisement for xmpp push 
+						and use it if possible. Still fall back to polling if there is no xmpp push 
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<false />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the 
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create 
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>20</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>5</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will 
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-accepts.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-accepts.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-accepts.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,522 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a Lion iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.7</string>
+
+					<!-- Client can poll the calendar home at some interval. This is 
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>300000</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
+						about calendar home changes instead of polling for them periodically. If 
+						this option is true, then look for the server advertisement for xmpp push 
+						and use it if possible. Still fall back to polling if there is no xmpp push 
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<true />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the 
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create 
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>20</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>5</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile invites some number of new attendees to new events. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the frequency at which new invitations will be sent out. -->
+							<key>sendInvitationDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- interval (in seconds). -->
+									<key>value</key>
+									<integer>150</integer>
+								</dict>
+							</dict>
+
+							<!-- Define the distribution of who will be invited to an event.
+							
+								When inviteeClumping is turned on each invitee is based on a sample of
+								users "close to" the organizer based on account index. If the clumping
+								is too "tight" for the requested number of attendees, then invites for
+								those larger numbers will simply fail (the sim will report that situation).
+								
+								When inviteeClumping is off invitees will be sampled across an entire
+								range of account indexes. In this case the distribution ought to be a
+								UniformIntegerDistribution with min=0 and max set to the number of accounts.
+							-->
+							<key>inviteeDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformIntegerDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- The minimum value (inclusive) of the uniform distribution. -->
+									<key>min</key>
+									<integer>0</integer>
+									<!-- The maximum value (exclusive) of the uniform distribution. -->
+									<key>max</key>
+									<integer>99</integer>
+								</dict>
+							</dict>
+
+							<key>inviteeClumping</key>
+							<true/>
+
+							<!-- Define the distribution of how many attendees will be invited to an event.
+							
+								LogNormal is the best fit to observed data.
+
+
+								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
+								mode should typically be 1, and mean whatever matches the user behavior.
+								Our typical mean is 6. 							
+							     -->
+							<key>inviteeCountDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- Number of attendees. -->
+									<key>value</key>
+									<integer>5</integer>
+								</dict>
+							</dict>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>100</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile accepts invitations to events, handles cancels, and
+					     handles replies received. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Accepter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define how long to wait after seeing a new invitation before
+								accepting it.
+
+								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
+								(i.e., half of the user have accepted by that time).								
+							-->
+							<key>acceptDelayDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformDiscreteDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- Set of values to use - will be chosen in random order. -->
+									<key>values</key>
+									<array>
+										<integer>0</integer>
+										<integer>5</integer>
+										<integer>10</integer>
+										<integer>15</integer>
+										<integer>20</integer>
+										<integer>25</integer>
+										<integer>30</integer>
+									</array>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- A task-creating profile, which will periodically create 
+						new tasks at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Tasker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how due times (DUE) for the randomly generated tasks 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>taskDueDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will 
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+
+		<!-- Determine the interval between client creation. -->
+		<key>arrivalInterval</key>
+		<integer>5</integer>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only-recurring.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only-recurring.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only-recurring.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,517 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a Lion iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.7</string>
+
+					<!-- Client can poll the calendar home at some interval. This is 
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>300000</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
+						about calendar home changes instead of polling for them periodically. If 
+						this option is true, then look for the server advertisement for xmpp push 
+						and use it if possible. Still fall back to polling if there is no xmpp push 
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<false />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the 
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create 
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>20</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>5</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile invites some number of new attendees to new events. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the frequency at which new invitations will be sent out. -->
+							<key>sendInvitationDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- interval (in seconds). -->
+									<key>value</key>
+									<integer>120</integer>
+								</dict>
+							</dict>
+
+							<!-- Define the distribution of who will be invited to an event.
+							
+								When inviteeClumping is turned on each invitee is based on a sample of
+								users "close to" the organizer based on account index. If the clumping
+								is too "tight" for the requested number of attendees, then invites for
+								those larger numbers will simply fail (the sim will report that situation).
+								
+								When inviteeClumping is off invitees will be sampled across an entire
+								range of account indexes. In this case the distribution ought to be a
+								UniformIntegerDistribution with min=0 and max set to the number of accounts.
+							-->
+							<key>inviteeDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformIntegerDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- The minimum value (inclusive) of the uniform distribution. -->
+									<key>min</key>
+									<integer>0</integer>
+									<!-- The maximum value (exclusive) of the uniform distribution. -->
+									<key>max</key>
+									<integer>99</integer>
+								</dict>
+							</dict>
+
+							<key>inviteeClumping</key>
+							<true/>
+
+							<!-- Define the distribution of how many attendees will be invited to an event.
+							
+								LogNormal is the best fit to observed data.
+
+
+								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
+								mode should typically be 1, and mean whatever matches the user behavior.
+								Our typical mean is 6. 							
+							     -->
+							<key>inviteeCountDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- Number of attendees. -->
+									<key>value</key>
+									<integer>5</integer>
+								</dict>
+							</dict>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>100</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile accepts invitations to events, handles cancels, and
+					     handles replies received. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Accepter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define how long to wait after seeing a new invitation before
+								accepting it.
+
+								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
+								(i.e., half of the user have accepted by that time).								
+							-->
+							<key>acceptDelayDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>300</integer>
+									<!-- median - 50% done-->
+									<key>median</key>
+									<integer>1800</integer>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- A task-creating profile, which will periodically create 
+						new tasks at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Tasker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how due times (DUE) for the randomly generated tasks 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>taskDueDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will 
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+
+		<!-- Determine the interval between client creation. -->
+		<key>arrivalInterval</key>
+		<integer>4</integer>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only.plist	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/invites-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,533 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+  -->
+
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+	<dict>
+		<!-- Define the kinds of software and user behavior the load simulation
+			will simulate. -->
+		<key>clients</key>
+
+		<!-- Have as many different kinds of software and user behavior configurations
+			as you want. Each is a dict -->
+		<array>
+
+			<dict>
+
+				<!-- Here is a Lion iCal simulator. -->
+				<key>software</key>
+				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
+
+				<!-- Arguments to use to initialize the client instance. -->
+				<key>params</key>
+				<dict>
+					<!-- Name that appears in logs. -->
+					<key>title</key>
+					<string>10.7</string>
+
+					<!-- Client can poll the calendar home at some interval. This is 
+						in seconds. -->
+					<key>calendarHomePollInterval</key>
+					<integer>300000</integer>
+
+					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
+						about calendar home changes instead of polling for them periodically. If 
+						this option is true, then look for the server advertisement for xmpp push 
+						and use it if possible. Still fall back to polling if there is no xmpp push 
+						advertised. -->
+					<key>supportPush</key>
+					<false />
+					<key>supportAmpPush</key>
+					<false />
+				</dict>
+
+				<!-- The profiles define certain types of user behavior on top of the 
+					client software being simulated. -->
+				<key>profiles</key>
+				<array>
+
+					<!-- First an event-creating profile, which will periodically create 
+						new events at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Eventer</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>20</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use
+								its client to create a new event. -->
+							<key>interval</key>
+							<integer>5</integer>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events
+								will be selected. This is an example of a "Distribution" parameter. The value
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps
+									in the near future, limited to certain days of the week and certain hours
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<true/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>25</integer>
+										<key>weekly</key>
+										<integer>25</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>0</integer>
+										<key>yearly</key>
+										<integer>0</integer>
+										<key>dailylimit</key>
+										<integer>0</integer>
+										<key>weeklylimit</key>
+										<integer>0</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>0</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile invites some number of new attendees to new events. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<true/>
+
+							<!-- Define the frequency at which new invitations will be sent out. -->
+							<key>sendInvitationDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- interval (in seconds). -->
+									<key>value</key>
+									<integer>120</integer>
+								</dict>
+							</dict>
+
+							<!-- Define the distribution of who will be invited to an event.
+							
+								When inviteeClumping is turned on each invitee is based on a sample of
+								users "close to" the organizer based on account index. If the clumping
+								is too "tight" for the requested number of attendees, then invites for
+								those larger numbers will simply fail (the sim will report that situation).
+								
+								When inviteeClumping is off invitees will be sampled across an entire
+								range of account indexes. In this case the distribution ought to be a
+								UniformIntegerDistribution with min=0 and max set to the number of accounts.
+							-->
+							<key>inviteeDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.UniformIntegerDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- The minimum value (inclusive) of the uniform distribution. -->
+									<key>min</key>
+									<integer>0</integer>
+									<!-- The maximum value (exclusive) of the uniform distribution. -->
+									<key>max</key>
+									<integer>99</integer>
+								</dict>
+							</dict>
+
+							<key>inviteeClumping</key>
+							<true/>
+
+							<!-- Define the distribution of how many attendees will be invited to an event.
+							
+								LogNormal is the best fit to observed data.
+
+
+								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
+								mode should typically be 1, and mean whatever matches the user behavior.
+								Our typical mean is 6. 							
+							     -->
+							<key>inviteeCountDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.FixedDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- Number of attendees. -->
+									<key>value</key>
+									<integer>5</integer>
+								</dict>
+							</dict>
+
+							<!-- Define how start times (DTSTART) for the randomly generated events 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>eventStartDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+
+							<!-- Define how recurrences are created. -->
+							<key>recurrenceDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized.  We have a fixed set of
+								     RRULEs defined for this distribution and pick each based on a
+								     weight. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.RecurrenceDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- False to disable RRULEs -->
+									<key>allowRecurrence</key>
+									<false/>
+
+									<!-- These are the weights for the specific set of RRULEs. -->
+									<key>weights</key>
+									<dict>
+										<!-- Half of all events will be non-recurring -->
+										<key>none</key>
+										<integer>50</integer>
+										
+										<!-- Daily and weekly are pretty common -->
+										<key>daily</key>
+										<integer>10</integer>
+										<key>weekly</key>
+										<integer>20</integer>
+										
+										<!-- Monthly, yearly, daily & weekly limit not so common -->
+										<key>monthly</key>
+										<integer>2</integer>
+										<key>yearly</key>
+										<integer>1</integer>
+										<key>dailylimit</key>
+										<integer>2</integer>
+										<key>weeklylimit</key>
+										<integer>5</integer>
+										
+										<!-- Work days pretty common -->
+										<key>workdays</key>
+										<integer>10</integer>
+									</dict>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- This profile accepts invitations to events, handles cancels, and
+					     handles replies received. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Accepter</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define how long to wait after seeing a new invitation before
+								accepting it.
+
+								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
+								(i.e., half of the user have accepted by that time).								
+							-->
+							<key>acceptDelayDistribution</key>
+							<dict>
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.LogNormalDistribution</string>
+								<key>params</key>
+								<dict>
+									<!-- mode - peak-->
+									<key>mode</key>
+									<integer>300</integer>
+									<!-- median - 50% done-->
+									<key>median</key>
+									<integer>1800</integer>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+					<!-- A task-creating profile, which will periodically create 
+						new tasks at a random time on a random calendar. -->
+					<dict>
+						<key>class</key>
+						<string>contrib.performance.loadtest.profiles.Tasker</string>
+
+						<key>params</key>
+						<dict>
+							<key>enabled</key>
+							<false/>
+
+							<!-- Define the interval (in seconds) at which this profile will use 
+								its client to create a new task. -->
+							<key>interval</key>
+							<integer>300</integer>
+
+							<!-- Define how due times (DUE) for the randomly generated tasks 
+								will be selected. This is an example of a "Distribution" parameter. The value 
+								for most "Distribution" parameters are interchangeable and extensible. -->
+							<key>taskDueDistribution</key>
+							<dict>
+
+								<!-- This distribution is pretty specialized. It produces timestamps 
+									in the near future, limited to certain days of the week and certain hours 
+									of the day. -->
+								<key>type</key>
+								<string>contrib.performance.loadtest.distributions.WorkDistribution</string>
+
+								<key>params</key>
+								<dict>
+									<!-- These are the days of the week the distribution will use. -->
+									<key>daysOfWeek</key>
+									<array>
+										<string>mon</string>
+										<string>tue</string>
+										<string>wed</string>
+										<string>thu</string>
+										<string>fri</string>
+									</array>
+
+									<!-- The earliest hour of a day at which an event might be scheduled. -->
+									<key>beginHour</key>
+									<integer>8</integer>
+
+									<!-- And the latest hour of a day (at which an event will be scheduled 
+										to begin!). -->
+									<key>endHour</key>
+									<integer>16</integer>
+
+									<!-- The timezone in which the event is scheduled. (XXX Does this 
+										really work right?) -->
+									<key>tzname</key>
+									<string>America/Los_Angeles</string>
+								</dict>
+							</dict>
+						</dict>
+					</dict>
+
+				</array>
+
+				<!-- Determine the frequency at which this client configuration will 
+					appear in the clients which are created by the load tester. -->
+				<key>weight</key>
+				<integer>1</integer>
+			</dict>
+		</array>
+	</dict>
+</plist>

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/clients.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/clients.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/clients.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,115 @@
+from contrib.performance.loadtest.clients import iOS_5, OS_X_10_6, OS_X_10_7, OS_X_10_11
+from contrib.performance.loadtest.profiles import (
+    Eventer, EventDeleter,
+    Titler,
+    Inviter, Emptier,
+    Tasker, TaskDeleter,
+    TaskTitler, TaskNoter, Completer, Prioritizer,
+
+    CalendarMaker, CalendarUpdater, CalendarSharer, CalendarDeleter
+)
+from contrib.performance.loadtest.distributions import FixedDistribution, BernoulliDistribution, NormalDistribution
+
+
+from preset_distributions import STANDARD_WORK_DISTRIBUTION, LOW_RECURRENCE_DISTRIBUTION, MEDIUM_RECURRENCE_DISTRIBUTION
+
+config = [
+    {
+        "software": OS_X_10_11,
+        #     title="10.11",
+        #     calendarHomePollInterval=5,
+        #     supportAmpPush=True,
+        #     ampPushHost="localhost",
+        #     ampPushPort62311
+        # )
+        "params": {
+            "title": "10.11",
+            "calendarHomePollInterval": 5,
+            "supportAmpPush": True,
+            "ampPushHost": "127.0.0.1",
+            "ampPushPort": 62311
+        },
+        "profiles": [
+            Eventer(enabled=True, interval=60, eventStartDistribution=STANDARD_WORK_DISTRIBUTION),
+            # Emptier(enabled=True, interval=30),
+            # Titler(enabled=True, interval=1, titleLengthDistribution=FixedDistribution(10)),
+            Inviter(enabled=True, interval=60, numInviteesDistribution=NormalDistribution(7, 2)),
+
+            # Tasker(enabled=False, interval=1),
+            # Completer(enabled=True, interval=0.5, completeLikelihood=BernoulliDistribution(0.5)),
+            # Prioritizer(enabled=True, interval=0.1),
+            # TaskTitler(enabled=True, interval=1),
+            # TaskNoter(enabled=True, interval=1),
+            # TaskDeleter(enabled=True, interval=1),
+
+            # CalendarMaker(enabled=True, interval=1),
+            # CalendarUpdater(enabled=True, interval=5),
+            # CalendarSharer(enabled=True, interval=30),
+            # CalendarDeleter(false=True, interval=30)
+        ],
+        "weight": 3
+    }
+]
+
+# # TBD what about multiple weights?
+# calendars_only_ideal = [
+#     OS_X_10_11(
+#         title="10.11",
+#         calendarHomePollInterval=5,
+#         supportAmpPush=True,
+#         ampPushHost="localhost",
+#         ampPushPort=62311,
+#         profiles=[
+#             CalendarMaker(enabled=True, interval=15),
+#             # CalendarUpdater(enabled=True, interval=5),
+#             # CalendarSharer(enabled=False, interval=30),
+#             # CalendarDeleter(enabled=False, interval=30)
+#         ]
+#     )
+# ]
+
+# event_updates_only = [
+#     {
+#         "software": OS_X_10_11,
+#         "params": {
+#             "title": "10.11",
+#             "calendarHomePollInterval": 5,
+#             "supportAmpPush": True,
+#             "ampPushHost": "localhost",
+#             "ampPushPort": 62311
+#         },
+#         "profiles": [
+#             ProfileType(Eventer, dict(
+#                 enabled=False,
+#                 interval=20,
+#                 eventStartDistribution=STANDARD_WORK_DISTRIBUTION,
+#                 recurrenceDistribution=MEDIUM_RECURRENCE_DISTRIBUTION
+#             )),
+#             ProfileType(EventerUpdater, dict(
+#                 enabled=True,
+#                 interval=5,
+#                 eventStartDistribution=STANDARD_WORK_DISTRIBUTION,
+#                 recurrenceDistribution=LOW_RECURRENCE_DISTRIBUTION
+#             )),
+#             ProfileType(RealisticInviter, dict(
+#                 enabled=False,
+#                 sendInvitationDistribution=LogNormalDistribution(mu=10, sigma=5),
+#                 inviteeDistribution=UniformIntegerDistribution(0, 99),
+#                 inviteeClumping=True,
+#                 inviteeCountDistribution=LogNormalDistribution(mode=1, median=6, maximum=100),
+#                 eventStartDistribution=STANDARD_WORK_DISTRIBUTION,
+#                 recurrenceDistribution=MEDIUM_RECURRENCE_DISTRIBUTION
+#             )),
+#             ProfileType(Accepter, dict(
+#                 enabled=False,
+#                 acceptDelayDistribution=LogNormalDistribution(mode=300, median=1800)
+#             )),
+#             ProfileType(Tasker, dict(
+#                 enabled=False,
+#                 interval=300,
+#                 taskDueDistribution=STANDARD_WORK_DISTRIBUTION
+#             ))
+#         ],
+#         "weight": 1
+#     }
+# ]

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config-dist.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config-dist.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config-dist.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,16 @@
+from contrib.performance.loadtest.settings.defaults import arrival, requestLogger, operationLogger, statisticsReporter, accounts
+
+from contrib.performance.loadtest.logger import EverythingLogger, MessageLogger
+
+NUM_WORKERS = 1
+
+config = {
+    "server": 'https://127.0.0.1:8443',
+    "webadminPort": 8080,
+    "serverStatsPort": 8100,
+    "serializationPath": '/tmp/sim',
+    "arrival": arrival,
+    "observers": [requestLogger, operationLogger, statisticsReporter, EverythingLogger(), MessageLogger()],
+    "records": accounts,
+    "workers": ["./bin/python contrib/performance/loadtest/ampsim.py"] * NUM_WORKERS,
+}

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/config.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,13 @@
+from contrib.performance.loadtest.settings.defaults import arrival, requestLogger, operationLogger, statisticsReporter, accounts
+
+from contrib.performance.loadtest.logger import EverythingLogger, MessageLogger
+
+config = {
+    "server": 'https://127.0.0.1:8443',
+    "webadminPort": 8080,
+    "serverStatsPort": {'server': 'localhost', 'Port': 8100},
+    "serializationPath": '/tmp/sim',
+    "arrival": arrival,
+    "observers": [requestLogger, operationLogger, statisticsReporter, EverythingLogger(), MessageLogger()],
+    "records": accounts
+}

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/defaults.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/defaults.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/defaults.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,24 @@
+from contrib.performance.loadtest.logger import ReportStatistics, RequestLogger, OperationLogger
+from contrib.performance.loadtest.records import recordsFromCSVFile
+from contrib.performance.loadtest.population import SmoothRampUp
+
+requestLogger = RequestLogger()
+operationLogger = OperationLogger(
+    thresholdsPath="contrib/performance/loadtest/thresholds.json",
+    lagCutoff=1.0,
+    failCutoff=1.0
+)
+statisticsReporter = ReportStatistics(
+    thresholdsPath="contrib/performance/loadtest/thresholds.json",
+    benchmarksPath="contrib/performance/loadtest/benchmarks.json",
+    failCutoff=1.0
+)
+
+arrival = SmoothRampUp(
+    groups=30,
+    groupSize=1,
+    interval=3,
+    clientsPerUser=1
+)
+
+accounts = recordsFromCSVFile("contrib/performance/loadtest/accounts.csv")

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/preset_distributions.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/preset_distributions.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/settings/preset_distributions.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,36 @@
+from contrib.performance.loadtest.distributions import WorkDistribution, RecurrenceDistribution
+
+STANDARD_WORK_DISTRIBUTION = WorkDistribution(
+    daysOfWeek=["mon", "tue", "wed", "thu", "fri"],
+    beginHour=8,
+    endHour=16,
+    tzname="America/Los_Angeles"
+)
+
+LOW_RECURRENCE_DISTRIBUTION = RecurrenceDistribution(
+    allowRecurrence=True,
+    weights={
+        "none": 50,
+        "daily": 25,
+        "weekly": 25,
+        "monthly": 0,
+        "yearly": 0,
+        "dailylimit": 0,
+        "weeklylimit": 0,
+        "workdays": 0
+    }
+)
+
+MEDIUM_RECURRENCE_DISTRIBUTION = RecurrenceDistribution(
+    allowRecurrence=True,
+    weights={
+        "none": 50,
+        "daily": 10,
+        "weekly": 20,
+        "monthly": 2,
+        "yearly": 1,
+        "dailylimit": 2,
+        "weeklylimit": 5,
+        "workdays": 10
+    }
+)
\ No newline at end of file

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sim.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sim.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/sim.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -45,58 +45,17 @@
 
 from twisted.web.server import Site
 
-from contrib.performance.loadtest.ical import OS_X_10_6
+from contrib.performance.loadtest.clients import OS_X_10_6
 from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
 from contrib.performance.loadtest.population import (
-    Populator, ProfileType, ClientType, PopulationParameters, SmoothRampUp,
+    ClientFactory, PopulationParameters, SmoothRampUp,
     CalendarClientSimulator)
+from contrib.performance.loadtest.config import Config
 from contrib.performance.loadtest.webadmin import LoadSimAdminResource
 
-
-
-class _DirectoryRecord(object):
-    def __init__(self, uid, password, commonName, email, guid):
-        self.uid = uid
-        self.password = password
-        self.commonName = commonName
-        self.email = email
-        self.guid = guid
-
-
-
 def safeDivision(value, total, factor=1):
     return value * factor / total if total else 0
 
-
-
-def generateRecords(
-    count, uidPattern="user%d", passwordPattern="user%d",
-    namePattern="User %d", emailPattern="user%d at example.com",
-    guidPattern="user%d"
-):
-    for i in xrange(count):
-        i += 1
-        uid = uidPattern % (i,)
-        password = passwordPattern % (i,)
-        name = namePattern % (i,)
-        email = emailPattern % (i,)
-        guid = guidPattern % (i,)
-        yield _DirectoryRecord(uid, password, name, email, guid)
-
-
-
-def recordsFromCSVFile(path):
-    if path:
-        pathObj = FilePath(path)
-    else:
-        pathObj = FilePath(__file__).sibling("accounts.csv")
-    return [
-        _DirectoryRecord(*line.decode('utf-8').split(u','))
-        for line
-        in pathObj.getContent().splitlines()]
-
-
-
 class LagTrackingReactor(object):
     """
     This reactor wraps another reactor and proxies all attribute
@@ -129,19 +88,31 @@
     Command line configuration options for the load simulator.
     """
     config = None
-    _defaultConfig = FilePath(__file__).sibling("config.plist")
-    _defaultClients = FilePath(__file__).sibling("clients.plist")
+    settings = FilePath(__file__).sibling("settings")
+    # plists = settings.child('alt-settings').child('plist')
+    # _defaultConfig = plists.child("config.plist")
+    # _defaultClients = plists.child("clients.plist")
+    # _defaultConfig = settings.child('settings.config')
+    # _defaultClients = settings.child('settings.clients')
+    _defaultConfig = 'contrib.performance.loadtest.settings.config'
+    _defaultClients = 'contrib.performance.loadtest.settings.clients'
 
+    optFlags = [
+        ("debug", "d", "Enable Deferred and Failure debugging"),
+        ("debug-deferred", None, "Enable only Deferred debugging"),
+        ("debug-failure", None, "Enable only Failure debugging"),
+        ("use-plist", None, "Interpret configuration files as XML property lists (default false)")
+    ]
+
     optParameters = [
         ("runtime", "t", None,
          "Specify the limit (seconds) on the time to run the simulation.",
          int),
         ("config", None, _defaultConfig,
-         "Configuration plist file name from which to read simulation parameters.",
-         FilePath),
+         "Configuration plist file name from which to read simulation parameters."),
         ("clients", None, _defaultClients,
-         "Configuration plist file name from which to read client parameters.",
-         FilePath),
+         "Configuration plist file name from which to read client parameters."),
+        ("logfile", "l", '-', FilePath)
     ]
 
 
@@ -181,36 +152,48 @@
 
 
     def postOptions(self):
-        try:
-            configFile = self['config'].open()
-        except IOError, e:
-            raise UsageError("--config %s: %s" % (
-                self['config'].path, e.strerror))
-        try:
+        """
+        Convert the given configuration files to dictionaries, respectively in
+        self.config and self.clients
+        """
+        configPath = self["config"]
+        clientsPath = self["clients"]
+        if self['use-plist']:
             try:
-                self.config = readPlist(configFile)
+                with open(configPath) as configFile: # Could raise an IOError
+                    self.config = readPlist(configFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError("--config %s: %s" % (configPath.path, e.strerror))
             except ExpatError, e:
-                raise UsageError("--config %s: %s" % (self['config'].path, e))
-        finally:
-            configFile.close()
+                raise UsageError("--config %s: %s" % (configPath.path, e))
 
-        try:
-            clientFile = self['clients'].open()
-        except IOError, e:
-            raise UsageError("--clients %s: %s" % (
-                self['clients'].path, e.strerror))
-        try:
             try:
-                client_config = readPlist(clientFile)
-                self.config["clients"] = client_config["clients"]
-                if "arrivalInterval" in client_config:
-                    self.config["arrival"]["params"]["interval"] = client_config["arrivalInterval"]
+                with open(clientsPath) as clientFile: # Could raise an IOError
+                    self.clients = readPlist(clientFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError("--clients %s: %s" % (clientsPath.path, e.strerror))
             except ExpatError, e:
-                raise UsageError("--clients %s: %s" % (self['clients'].path, e))
-        finally:
-            clientFile.close()
+                raise UsageError("--clients %s: %s" % (clientsPath.path, e))
 
+            self.config["clients"] = self.clients["clients"]
 
+        else:
+            from importlib import import_module
+            try:
+                config = import_module(configPath) # Could raise ImportError
+                self.config = config.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError("--config %s: %s" % (configPath, e))
+            try:
+                clients = import_module(clientsPath) # Could raise ImportError
+                self.clients = clients.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError("--clients %s: %s" % (clientsPath, e))
+
+        self.configObj = Config()
+        self.configObj.populateFrom(self.config, self.clients)
+
+
 Arrival = namedtuple('Arrival', 'factory parameters')
 
 
@@ -224,17 +207,18 @@
     @type arrival: L{Arrival}
     @type parameters: L{PopulationParameters}
 
-    @ivar records: A C{list} of L{_DirectoryRecord} instances giving
+    @ivar records: A C{list} of L{DirectoryRecord} instances giving
         user information about the accounts on the server being put
         under load.
     """
-    def __init__(self, server, principalPathTemplate, webadminPort, serverStats, serializationPath, arrival, parameters, observers=None,
+    def __init__(self, server, webadminPort, serverStats, serializationPath, arrival, parameters, observers=None,
                  records=None, reactor=None, runtime=None, workers=None,
                  configTemplate=None, workerID=None, workerCount=1):
+        if configTemplate == {}:
+            raise Exception('Got here!')
         if reactor is None:
             from twisted.internet import reactor
         self.server = server
-        self.principalPathTemplate = principalPathTemplate
         self.webadminPort = webadminPort
         self.serverStats = serverStats
         self.serializationPath = serializationPath
@@ -262,7 +246,7 @@
         except UsageError, e:
             raise SystemExit(str(e))
 
-        return cls.fromConfig(options.config, options['runtime'], output)
+        return cls.fromConfigObject(options.configObj, options['runtime'], output)
 
 
     @classmethod
@@ -270,6 +254,8 @@
         """
         Create a L{LoadSimulator} from a parsed instance of a configuration
         property list.
+
+        @type{config} L{Config} object
         """
 
         workers = config.get("workers")
@@ -279,7 +265,6 @@
             workerCount = config.get("workerCount", 1)
             configTemplate = None
             server = config.get('server', 'http://127.0.0.1:8008')
-            principalPathTemplate = config.get('principalPathTemplate', '/principals/users/%s/')
             serializationPath = None
 
             if 'clientDataSerialization' in config:
@@ -306,6 +291,14 @@
             parameters = PopulationParameters()
             if 'clients' in config:
                 for clientConfig in config['clients']:
+                    # parameters.addClient(
+                    #     clientConfig["weight"],
+                    #     ClientType(
+                    #         clientConfig["software"],
+                    #         clientConfig["params"],
+                    #         clientConfig["profiles"]
+                    #     )
+                    # )
                     parameters.addClient(
                         clientConfig["weight"],
                         ClientType(
@@ -316,7 +309,8 @@
                                     namedAny(profile["class"]),
                                     cls._convertParams(profile["params"])
                                 ) for profile in clientConfig["profiles"]
-                            ]))
+                            ])),
+                        
             if not parameters.clients:
                 parameters.addClient(1,
                                      ClientType(OS_X_10_6, {},
@@ -324,7 +318,6 @@
         else:
             # Manager / observer process.
             server = ''
-            principalPathTemplate = ''
             serializationPath = None
             arrival = None
             parameters = None
@@ -359,7 +352,6 @@
 
         return cls(
             server,
-            principalPathTemplate,
             webadminPort,
             serverStats,
             serializationPath,
@@ -368,7 +360,6 @@
             observers=observers,
             records=records,
             runtime=runtime,
-            reactor=reactor,
             workers=workers,
             configTemplate=configTemplate,
             workerID=workerID,
@@ -377,30 +368,39 @@
 
 
     @classmethod
-    def _convertParams(cls, params):
-        """
-        Find parameter values which should be more structured than plistlib is
-        capable of constructing and replace them with the more structured form.
+    def fromConfigObject(cls, config, runtime=None, output=stdout):
+        # if config.isManaging:
+        #     observers = 
 
-        Specifically, find keys that end with C{"Distribution"} and convert
-        them into some kind of distribution object using the associated
-        dictionary of keyword arguments.
-        """
-        for k, v in params.iteritems():
-            if k.endswith('Distribution'):
-                params[k] = cls._convertDistribution(v)
-        return params
+        # if config.isWorking:
+        #     simulator = CalendarClientSimulator(
+        #         config.records,
+        #         config.parameters,
+        #         config.reactor,
+        #         config.server,
+        #         config.serializationPath,
+        #         config.workerID,
+        #         config.workerCount,
+        #     )
+        #     arrival = config.arrival
 
+        return cls(
+            config.server,
+            config.webadminPort,
+            config.serverStats,
+            config.serializationPath,
+            config.arrival,
+            config.parameters,
+            observers=config.observers,
+            records=config.records,
+            runtime=runtime,
+            configTemplate=config.serializeForWorker,
+            workers=config.workers,
+            workerID=config.workerID,
+            workerCount=config.workerCount,
+        )
 
-    @classmethod
-    def _convertDistribution(cls, value):
-        """
-        Construct and return a new distribution object using the type and
-        params specified by C{value}.
-        """
-        return namedAny(value['type'])(**value['params'])
 
-
     @classmethod
     def main(cls, args=None):
         simulator = cls.fromCommandLine(args)
@@ -408,14 +408,11 @@
 
 
     def createSimulator(self):
-        populator = Populator(Random())
         return CalendarClientSimulator(
             self.records,
-            populator,
             self.parameters,
             self.reactor,
             self.server,
-            self.principalPathTemplate,
             self.serializationPath,
             self.workerID,
             self.workerCount,
@@ -423,7 +420,9 @@
 
 
     def createArrivalPolicy(self):
-        return self.arrival.factory(self.reactor, **self.arrival.parameters)
+        # print(self.arrival.parameters)
+        # return zory(self.reactor, **self.arrival.parameters)
+        return self.arrival
 
 
     def serviceClasses(self):
@@ -431,16 +430,15 @@
         Return a list of L{SimService} subclasses for C{attachServices} to
         instantiate and attach to the reactor.
         """
-        if self.workers is not None:
-            return [
-                ObserverService,
-                WorkerSpawnerService,
-                ReporterService,
-            ]
+        if self.workers:
+            PrimaryService = WorkerSpawnerService
+        else:
+            PrimaryService = SimulatorService
+
         return [
             ObserverService,
-            SimulatorService,
             ReporterService,
+            PrimaryService
         ]
 
 
@@ -509,6 +507,7 @@
             data = ""
             while not data.endswith("\n"):
                 d = s.recv(1024)
+                print("Received data from stats socket: ", d)
                 if d:
                     data += d
                 else:
@@ -556,6 +555,9 @@
         """
         super(ObserverService, self).startService()
         for obs in self.loadsim.observers:
+            import random
+            val = random.random()
+            msg(type='log', text='Adding observer: ' + obs.__class__.__name__, val=str(val))
             addObserver(obs.observe)
 
 
@@ -573,16 +575,19 @@
     """
 
     def startService(self):
+        print("Starting simulator service")
         super(SimulatorService, self).startService()
         self.clientsim = self.loadsim.createSimulator()
         arrivalPolicy = self.loadsim.createArrivalPolicy()
-        arrivalPolicy.run(self.clientsim)
+        arrivalPolicy.run(self.loadsim.reactor, self.clientsim)
+        # self.loadsim.arrival.run(self.loadsim.reactor, self.loadsim.simulator)
 
 
     @inlineCallbacks
     def stopService(self):
         yield super(SimulatorService, self).stopService()
         yield self.clientsim.stop()
+        # yield self.loadsim.simulator.stop()
 
 
 
@@ -596,6 +601,7 @@
         """
         Start observing.
         """
+        print("Starting reporter service")
         super(ReporterService, self).startService()
         self.loadsim.reporter = self
 
@@ -670,6 +676,7 @@
         super(WorkerSpawnerService, self).startService()
         self.bridges = []
         for workerID, worker in enumerate(self.loadsim.workers):
+            print("Building bridge for #" + str(workerID))
             bridge = ProcessProtocolBridge(
                 self, Manager(self.loadsim, workerID, len(self.loadsim.workers),
                               self.output)

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/event-updates-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/event-updates-only.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/event-updates-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,543 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a Lion iCal simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the client instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-
-					<!-- Client can poll the calendar home at some interval. This is 
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>300000</integer>
-
-					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
-						about calendar home changes instead of polling for them periodically. If 
-						this option is true, then look for the server advertisement for xmpp push 
-						and use it if possible. Still fall back to polling if there is no xmpp push 
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-					<key>supportAmpPush</key>
-					<false />
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the 
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create 
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>20</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.NormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mu gives the mean of the normal distribution (in seconds). -->
-									<key>mu</key>
-									<integer>10</integer>
-
-									<!-- and sigma gives its standard deviation. -->
-									<key>sigma</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>1</integer>
-									<!-- mean - average-->
-									<key>median</key>
-									<integer>6</integer>
-									<!-- maximum -->
-									<key>maximum</key>
-									<real>100</real>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>300</integer>
-									<!-- median - 50% done-->
-									<key>median</key>
-									<integer>1800</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create 
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will 
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-	</dict>
-</plist>

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/events-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/events-only.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/events-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,543 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a Lion iCal simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the client instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-
-					<!-- Client can poll the calendar home at some interval. This is 
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>300000</integer>
-
-					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
-						about calendar home changes instead of polling for them periodically. If 
-						this option is true, then look for the server advertisement for xmpp push 
-						and use it if possible. Still fall back to polling if there is no xmpp push 
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-					<key>supportAmpPush</key>
-					<false />
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the 
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create 
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>20</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.NormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mu gives the mean of the normal distribution (in seconds). -->
-									<key>mu</key>
-									<integer>10</integer>
-
-									<!-- and sigma gives its standard deviation. -->
-									<key>sigma</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>1</integer>
-									<!-- mean - average-->
-									<key>median</key>
-									<integer>6</integer>
-									<!-- maximum -->
-									<key>maximum</key>
-									<real>100</real>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>300</integer>
-									<!-- median - 50% done-->
-									<key>median</key>
-									<integer>1800</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create 
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will 
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-	</dict>
-</plist>

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-accepts.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-accepts.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-accepts.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,522 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a Lion iCal simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the client instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-
-					<!-- Client can poll the calendar home at some interval. This is 
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>300000</integer>
-
-					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
-						about calendar home changes instead of polling for them periodically. If 
-						this option is true, then look for the server advertisement for xmpp push 
-						and use it if possible. Still fall back to polling if there is no xmpp push 
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-					<key>supportAmpPush</key>
-					<true />
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the 
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create 
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>20</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- interval (in seconds). -->
-									<key>value</key>
-									<integer>150</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- Number of attendees. -->
-									<key>value</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>100</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformDiscreteDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- Set of values to use - will be chosen in random order. -->
-									<key>values</key>
-									<array>
-										<integer>0</integer>
-										<integer>5</integer>
-										<integer>10</integer>
-										<integer>15</integer>
-										<integer>20</integer>
-										<integer>25</integer>
-										<integer>30</integer>
-									</array>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create 
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will 
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-
-		<!-- Determine the interval between client creation. -->
-		<key>arrivalInterval</key>
-		<integer>5</integer>
-	</dict>
-</plist>

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only-recurring.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only-recurring.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only-recurring.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,517 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a Lion iCal simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the client instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-
-					<!-- Client can poll the calendar home at some interval. This is 
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>300000</integer>
-
-					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
-						about calendar home changes instead of polling for them periodically. If 
-						this option is true, then look for the server advertisement for xmpp push 
-						and use it if possible. Still fall back to polling if there is no xmpp push 
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-					<key>supportAmpPush</key>
-					<false />
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the 
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create 
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>20</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- interval (in seconds). -->
-									<key>value</key>
-									<integer>120</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- Number of attendees. -->
-									<key>value</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>100</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>300</integer>
-									<!-- median - 50% done-->
-									<key>median</key>
-									<integer>1800</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create 
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will 
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-
-		<!-- Determine the interval between client creation. -->
-		<key>arrivalInterval</key>
-		<integer>4</integer>
-	</dict>
-</plist>

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only.plist
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only.plist	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/standard-configs/invites-only.plist	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,533 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
-    Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-  -->
-
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-	<dict>
-		<!-- Define the kinds of software and user behavior the load simulation
-			will simulate. -->
-		<key>clients</key>
-
-		<!-- Have as many different kinds of software and user behavior configurations
-			as you want. Each is a dict -->
-		<array>
-
-			<dict>
-
-				<!-- Here is a Lion iCal simulator. -->
-				<key>software</key>
-				<string>contrib.performance.loadtest.ical.OS_X_10_7</string>
-
-				<!-- Arguments to use to initialize the client instance. -->
-				<key>params</key>
-				<dict>
-					<!-- Name that appears in logs. -->
-					<key>title</key>
-					<string>10.7</string>
-
-					<!-- Client can poll the calendar home at some interval. This is 
-						in seconds. -->
-					<key>calendarHomePollInterval</key>
-					<integer>300000</integer>
-
-					<!-- If the server advertises xmpp push, OS X 10.6 can wait for notifications 
-						about calendar home changes instead of polling for them periodically. If 
-						this option is true, then look for the server advertisement for xmpp push 
-						and use it if possible. Still fall back to polling if there is no xmpp push 
-						advertised. -->
-					<key>supportPush</key>
-					<false />
-					<key>supportAmpPush</key>
-					<false />
-				</dict>
-
-				<!-- The profiles define certain types of user behavior on top of the 
-					client software being simulated. -->
-				<key>profiles</key>
-				<array>
-
-					<!-- First an event-creating profile, which will periodically create 
-						new events at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Eventer</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>20</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile will create a new event, and then periodically update the ACKNOWLEDGED property. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.EventUpdater</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use
-								its client to create a new event. -->
-							<key>interval</key>
-							<integer>5</integer>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events
-								will be selected. This is an example of a "Distribution" parameter. The value
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps
-									in the near future, limited to certain days of the week and certain hours
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<true/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>25</integer>
-										<key>weekly</key>
-										<integer>25</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>0</integer>
-										<key>yearly</key>
-										<integer>0</integer>
-										<key>dailylimit</key>
-										<integer>0</integer>
-										<key>weeklylimit</key>
-										<integer>0</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>0</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile invites some number of new attendees to new events. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.RealisticInviter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<true/>
-
-							<!-- Define the frequency at which new invitations will be sent out. -->
-							<key>sendInvitationDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- interval (in seconds). -->
-									<key>value</key>
-									<integer>120</integer>
-								</dict>
-							</dict>
-
-							<!-- Define the distribution of who will be invited to an event.
-							
-								When inviteeClumping is turned on each invitee is based on a sample of
-								users "close to" the organizer based on account index. If the clumping
-								is too "tight" for the requested number of attendees, then invites for
-								those larger numbers will simply fail (the sim will report that situation).
-								
-								When inviteeClumping is off invitees will be sampled across an entire
-								range of account indexes. In this case the distribution ought to be a
-								UniformIntegerDistribution with min=0 and max set to the number of accounts.
-							-->
-							<key>inviteeDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.UniformIntegerDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- The minimum value (inclusive) of the uniform distribution. -->
-									<key>min</key>
-									<integer>0</integer>
-									<!-- The maximum value (exclusive) of the uniform distribution. -->
-									<key>max</key>
-									<integer>99</integer>
-								</dict>
-							</dict>
-
-							<key>inviteeClumping</key>
-							<true/>
-
-							<!-- Define the distribution of how many attendees will be invited to an event.
-							
-								LogNormal is the best fit to observed data.
-
-
-								For LogNormal "mode" is the peak, "mean" is the mean value.	For invites,
-								mode should typically be 1, and mean whatever matches the user behavior.
-								Our typical mean is 6. 							
-							     -->
-							<key>inviteeCountDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.FixedDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- Number of attendees. -->
-									<key>value</key>
-									<integer>5</integer>
-								</dict>
-							</dict>
-
-							<!-- Define how start times (DTSTART) for the randomly generated events 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>eventStartDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-
-							<!-- Define how recurrences are created. -->
-							<key>recurrenceDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized.  We have a fixed set of
-								     RRULEs defined for this distribution and pick each based on a
-								     weight. -->
-								<key>type</key>
-								<string>contrib.performance.stats.RecurrenceDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- False to disable RRULEs -->
-									<key>allowRecurrence</key>
-									<false/>
-
-									<!-- These are the weights for the specific set of RRULEs. -->
-									<key>weights</key>
-									<dict>
-										<!-- Half of all events will be non-recurring -->
-										<key>none</key>
-										<integer>50</integer>
-										
-										<!-- Daily and weekly are pretty common -->
-										<key>daily</key>
-										<integer>10</integer>
-										<key>weekly</key>
-										<integer>20</integer>
-										
-										<!-- Monthly, yearly, daily & weekly limit not so common -->
-										<key>monthly</key>
-										<integer>2</integer>
-										<key>yearly</key>
-										<integer>1</integer>
-										<key>dailylimit</key>
-										<integer>2</integer>
-										<key>weeklylimit</key>
-										<integer>5</integer>
-										
-										<!-- Work days pretty common -->
-										<key>workdays</key>
-										<integer>10</integer>
-									</dict>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- This profile accepts invitations to events, handles cancels, and
-					     handles replies received. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Accepter</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define how long to wait after seeing a new invitation before
-								accepting it.
-
-								For LogNormal "mode" is the peak, "median" is the 50% cummulative value
-								(i.e., half of the user have accepted by that time).								
-							-->
-							<key>acceptDelayDistribution</key>
-							<dict>
-								<key>type</key>
-								<string>contrib.performance.stats.LogNormalDistribution</string>
-								<key>params</key>
-								<dict>
-									<!-- mode - peak-->
-									<key>mode</key>
-									<integer>300</integer>
-									<!-- median - 50% done-->
-									<key>median</key>
-									<integer>1800</integer>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-					<!-- A task-creating profile, which will periodically create 
-						new tasks at a random time on a random calendar. -->
-					<dict>
-						<key>class</key>
-						<string>contrib.performance.loadtest.profiles.Tasker</string>
-
-						<key>params</key>
-						<dict>
-							<key>enabled</key>
-							<false/>
-
-							<!-- Define the interval (in seconds) at which this profile will use 
-								its client to create a new task. -->
-							<key>interval</key>
-							<integer>300</integer>
-
-							<!-- Define how due times (DUE) for the randomly generated tasks 
-								will be selected. This is an example of a "Distribution" parameter. The value 
-								for most "Distribution" parameters are interchangeable and extensible. -->
-							<key>taskDueDistribution</key>
-							<dict>
-
-								<!-- This distribution is pretty specialized. It produces timestamps 
-									in the near future, limited to certain days of the week and certain hours 
-									of the day. -->
-								<key>type</key>
-								<string>contrib.performance.stats.WorkDistribution</string>
-
-								<key>params</key>
-								<dict>
-									<!-- These are the days of the week the distribution will use. -->
-									<key>daysOfWeek</key>
-									<array>
-										<string>mon</string>
-										<string>tue</string>
-										<string>wed</string>
-										<string>thu</string>
-										<string>fri</string>
-									</array>
-
-									<!-- The earliest hour of a day at which an event might be scheduled. -->
-									<key>beginHour</key>
-									<integer>8</integer>
-
-									<!-- And the latest hour of a day (at which an event will be scheduled 
-										to begin!). -->
-									<key>endHour</key>
-									<integer>16</integer>
-
-									<!-- The timezone in which the event is scheduled. (XXX Does this 
-										really work right?) -->
-									<key>tzname</key>
-									<string>America/Los_Angeles</string>
-								</dict>
-							</dict>
-						</dict>
-					</dict>
-
-				</array>
-
-				<!-- Determine the frequency at which this client configuration will 
-					appear in the clients which are created by the load tester. -->
-				<key>weight</key>
-				<integer>1</integer>
-			</dict>
-		</array>
-	</dict>
-</plist>

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/subscribe.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/subscribe.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/subscribe.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,30 +0,0 @@
-
-class Subscription(object):
-    def __init__(self, periodical, subscriber):
-        self.periodical = periodical
-        self.subscriber = subscriber
-
-
-    def cancel(self):
-        self.periodical.subscriptions.remove(self)
-
-
-    def issue(self, issue):
-        self.subscriber(issue)
-
-
-
-class Periodical(object):
-    def __init__(self):
-        self.subscriptions = []
-
-
-    def subscribe(self, who):
-        subscription = Subscription(self, who)
-        self.subscriptions.append(subscription)
-        return subscription
-
-
-    def issue(self, issue):
-        for subscr in self.subscriptions:
-            subscr.issue(issue)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/templates.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/templates.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/templates.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,72 @@
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+"""
+A list of predefined component templates for use in the client sim
+"""
+
+from twistedcaldav.ical import Component
+
+
+# Default Event
+eventTemplate = Component.fromString("""
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.11//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+TRANSP:OPAQUE
+SUMMARY:Sample Event
+UID:00000000-0000-0000-0000-000000000000
+CREATED:00000000T000000Z
+DTSTAMP:00000000T000000Z
+DTSTART:00000000T000000
+DTEND:00000000T000000
+END:VEVENT
+END:VCALENDAR
+""")
+
+
+# Default Task
+taskTemplate = Component.fromString("""\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.11//EN
+CALSCALE:GREGORIAN
+BEGIN:VTODO
+SUMMARY:Sample Task
+UID:00000000-0000-0000-0000-000000000000
+CREATED:00000000T000000Z
+DTSTAMP:00000000T000000Z
+END:VTODO
+END:VCALENDAR
+""")
+
+
+# Default Alarm
+alarmTemplate = Component.fromString("""\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.11//EN
+CALSCALE:GREGORIAN
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-PT5M
+UID:00000000-0000-0000-0000-000000000000
+DESCRIPTION:Sample Alarm
+END:VALARM
+END:VCALENDAR
+""")

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_ical.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_ical.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_ical.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,2022 +0,0 @@
-##
-# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-from caldavclientlibrary.protocol.caldav.definitions import caldavxml
-from caldavclientlibrary.protocol.caldav.definitions import csxml
-from caldavclientlibrary.protocol.url import URL
-from caldavclientlibrary.protocol.webdav.definitions import davxml
-
-from contrib.performance.httpclient import MemoryConsumer, StringProducer
-from contrib.performance.loadtest.ical import XMPPPush, Event, Calendar, OS_X_10_6
-from contrib.performance.loadtest.sim import _DirectoryRecord
-
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
-from twisted.internet.protocol import ProtocolToConsumerAdapter
-from twisted.python.failure import Failure
-from twisted.trial.unittest import TestCase
-from twisted.web.client import ResponseDone
-from twisted.web.http import OK, NO_CONTENT, CREATED, MULTI_STATUS
-from twisted.web.http_headers import Headers
-
-from twistedcaldav.ical import Component
-from twistedcaldav.timezones import TimezoneCache
-
-import json
-import os
-
-EVENT_UID = 'D94F247D-7433-43AF-B84B-ADD684D023B0'
-
-EVENT = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20101018T155454Z
-UID:%(UID)s
-DTEND;TZID=America/New_York:20101028T130000
-ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
- TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
- m
-ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:user01@
- example.com
-TRANSP:OPAQUE
-SUMMARY:Attended Event
-DTSTART;TZID=America/New_York:20101028T120000
-DTSTAMP:20101018T155513Z
-ORGANIZER;CN="User 01":mailto:user01 at example.com
-SEQUENCE:3
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n") % {'UID': EVENT_UID}
-
-EVENT_INVITE = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-X-LIC-LOCATION:America/New_York
-BEGIN:STANDARD
-DTSTART:18831118T120358
-RDATE:18831118T120358
-TZNAME:EST
-TZOFFSETFROM:-045602
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19180331T020000
-RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19181027T020000
-RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:STANDARD
-DTSTART:19200101T000000
-RDATE:19200101T000000
-RDATE:19420101T000000
-RDATE:19460101T000000
-RDATE:19670101T000000
-TZNAME:EST
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19200328T020000
-RDATE:19200328T020000
-RDATE:19740106T020000
-RDATE:19750223T020000
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19201031T020000
-RDATE:19201031T020000
-RDATE:19450930T020000
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19210424T020000
-RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19210925T020000
-RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19420209T020000
-RDATE:19420209T020000
-TZNAME:EWT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19450814T190000
-RDATE:19450814T190000
-TZNAME:EPT
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19460428T020000
-RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19460929T020000
-RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:STANDARD
-DTSTART:19551030T020000
-RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19760425T020000
-RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19870405T020000
-RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-CREATED:20101018T155454Z
-UID:%(UID)s
-DTEND;TZID=America/New_York:20101028T130000
-ATTENDEE;CN="User 02";CUTYPE=INDIVIDUAL;EMAIL="user02 at example.com";PARTS
- TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user02 at example.co
- m
-ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
- TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
- m
-ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:user01
-TRANSP:OPAQUE
-SUMMARY:Attended Event
-DTSTART;TZID=America/New_York:20101028T120000
-DTSTAMP:20101018T155513Z
-ORGANIZER;CN="User 01":urn:uuid:user01
-SEQUENCE:3
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n") % {'UID': EVENT_UID}
-
-EVENT_AND_TIMEZONE = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-X-LIC-LOCATION:America/New_York
-BEGIN:STANDARD
-DTSTART:18831118T120358
-RDATE:18831118T120358
-TZNAME:EST
-TZOFFSETFROM:-045602
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19180331T020000
-RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19181027T020000
-RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:STANDARD
-DTSTART:19200101T000000
-RDATE:19200101T000000
-RDATE:19420101T000000
-RDATE:19460101T000000
-RDATE:19670101T000000
-TZNAME:EST
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19200328T020000
-RDATE:19200328T020000
-RDATE:19740106T020000
-RDATE:19750223T020000
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19201031T020000
-RDATE:19201031T020000
-RDATE:19450930T020000
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19210424T020000
-RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19210925T020000
-RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19420209T020000
-RDATE:19420209T020000
-TZNAME:EWT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19450814T190000
-RDATE:19450814T190000
-TZNAME:EPT
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19460428T020000
-RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19460929T020000
-RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:STANDARD
-DTSTART:19551030T020000
-RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19670430T020000
-RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19671029T020000
-RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:19760425T020000
-RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:19870405T020000
-RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-CREATED:20101018T155454Z
-UID:%(UID)s
-DTEND;TZID=America/New_York:20101028T130000
-ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
- TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
- m
-ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:user01@
- example.com
-TRANSP:OPAQUE
-SUMMARY:Attended Event
-DTSTART;TZID=America/New_York:20101028T120000
-DTSTAMP:20101018T155513Z
-ORGANIZER;CN="User 01":mailto:user01 at example.com
-SEQUENCE:3
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n") % {'UID': EVENT_UID}
-
-
-
-class EventTests(TestCase):
-    """
-    Tests for L{Event}.
-    """
-    def test_uid(self):
-        """
-        When the C{vevent} attribute of an L{Event} instance is set,
-        L{Event.getUID} returns the UID value from it.
-        """
-        event = Event(None, u'/foo/bar', u'etag', Component.fromString(EVENT))
-        self.assertEquals(event.getUID(), EVENT_UID)
-
-
-    def test_withoutUID(self):
-        """
-        When an L{Event} has a C{vevent} attribute set to C{None},
-        L{Event.getUID} returns C{None}.
-        """
-        event = Event(None, u'/bar/baz', u'etag')
-        self.assertIdentical(event.getUID(), None)
-
-
-
-PRINCIPAL_PROPFIND_RESPONSE = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/principals/__uids__/user01/</href>
-    <propstat>
-      <prop>
-        <principal-collection-set>
-          <href>/principals/</href>
-        </principal-collection-set>
-        <calendar-home-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01</href>
-        </calendar-home-set>
-        <calendar-user-address-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/principals/__uids__/user01/</href>
-          <href xmlns='DAV:'>/principals/users/user01/</href>
-        </calendar-user-address-set>
-        <schedule-inbox-URL xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/inbox/</href>
-        </schedule-inbox-URL>
-        <schedule-outbox-URL xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/outbox/</href>
-        </schedule-outbox-URL>
-        <dropbox-home-URL xmlns='http://calendarserver.org/ns/'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/dropbox/</href>
-        </dropbox-home-URL>
-        <notification-URL xmlns='http://calendarserver.org/ns/'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/notification/</href>
-        </notification-URL>
-        <displayname>User 01</displayname>
-        <principal-URL>
-          <href>/principals/__uids__/user01/</href>
-        </principal-URL>
-        <supported-report-set>
-          <supported-report>
-            <report>
-              <acl-principal-prop-set/>
-            </report>
-          </supported-report>
-          <supported-report>
-            <report>
-              <principal-match/>
-            </report>
-          </supported-report>
-          <supported-report>
-            <report>
-              <principal-property-search/>
-            </report>
-          </supported-report>
-          <supported-report>
-            <report>
-              <expand-property/>
-            </report>
-          </supported-report>
-        </supported-report-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-</multistatus>
-"""
-
-_CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/calendars/__uids__/user01/</href>
-    <propstat>
-      <prop>
-        %(xmpp)s
-        <displayname>User 01</displayname>
-        <resourcetype>
-          <collection/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-        </current-user-privilege-set>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'>/Some/Unique/Value</pushkey>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <getctag xmlns='http://calendarserver.org/ns/'/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/notification/</href>
-    <propstat>
-      <prop>
-        <displayname>notification</displayname>
-        <resourcetype>
-          <collection/>
-          <notification xmlns='http://calendarserver.org/ns/'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-        </current-user-privilege-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <getctag xmlns='http://calendarserver.org/ns/'/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/dropbox/</href>
-    <propstat>
-      <prop>
-        <resourcetype>
-          <collection/>
-          <dropbox-home xmlns='http://calendarserver.org/ns/'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-        </current-user-privilege-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <getctag xmlns='http://calendarserver.org/ns/'/>
-        <displayname/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/calendar/</href>
-    <propstat>
-      <prop>
-        <getctag xmlns='http://calendarserver.org/ns/'>c2696540-4c4c-4a31-adaf-c99630776828#3</getctag>
-        <displayname>calendar</displayname>
-        <calendar-color xmlns='http://apple.com/ns/ical/'>#0252D4FF</calendar-color>
-        <calendar-order xmlns='http://apple.com/ns/ical/'>1</calendar-order>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <comp name='VEVENT'/>
-          <comp name='VTODO'/>
-          <comp name='VTIMEZONE'/>
-          <comp name='VFREEBUSY'/>
-        </supported-calendar-component-set>
-        <resourcetype>
-          <collection/>
-          <calendar xmlns='urn:ietf:params:xml:ns:caldav'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'>
-          <opaque/>
-        </schedule-calendar-transp>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-BEGIN:DAYLIGHT
-TZOFFSETFROM:-0500
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-DTSTART:20070311T020000
-TZNAME:EDT
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-TZOFFSETFROM:-0400
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-DTSTART:20071104T020000
-TZNAME:EST
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-]]></calendar-timezone>
-        <current-user-privilege-set>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-        </current-user-privilege-set>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/outbox/</href>
-    <propstat>
-      <prop>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <comp name='VEVENT'/>
-          <comp name='VTODO'/>
-          <comp name='VTIMEZONE'/>
-          <comp name='VFREEBUSY'/>
-        </supported-calendar-component-set>
-        <resourcetype>
-          <collection/>
-          <schedule-outbox xmlns='urn:ietf:params:xml:ns:caldav'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-          <privilege>
-            <schedule-send xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-        </current-user-privilege-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <getctag xmlns='http://calendarserver.org/ns/'/>
-        <displayname/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/freebusy</href>
-    <propstat>
-      <prop>
-        <resourcetype>
-          <free-busy-url xmlns='http://calendarserver.org/ns/'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <schedule-deliver xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-        </current-user-privilege-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <getctag xmlns='http://calendarserver.org/ns/'/>
-        <displayname/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/calendars/__uids__/user01/inbox/</href>
-    <propstat>
-      <prop>
-        <getctag xmlns='http://calendarserver.org/ns/'>a483dab3-1391-445b-b1c3-5ae9dfc81c2f#0</getctag>
-        <displayname>inbox</displayname>
-        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <comp name='VEVENT'/>
-          <comp name='VTODO'/>
-          <comp name='VTIMEZONE'/>
-          <comp name='VFREEBUSY'/>
-        </supported-calendar-component-set>
-        <resourcetype>
-          <collection/>
-          <schedule-inbox xmlns='urn:ietf:params:xml:ns:caldav'/>
-        </resourcetype>
-        <owner>
-          <href>/principals/__uids__/user01/</href>
-        </owner>
-        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/calendar</href>
-        </calendar-free-busy-set>
-        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'>
-          <href xmlns='DAV:'>/calendars/__uids__/user01/calendar</href>
-        </schedule-default-calendar-URL>
-        <quota-available-bytes>104855434</quota-available-bytes>
-        <quota-used-bytes>2166</quota-used-bytes>
-        <current-user-privilege-set>
-          <privilege>
-            <schedule-deliver xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-          <privilege>
-            <all/>
-          </privilege>
-          <privilege>
-            <read/>
-          </privilege>
-          <privilege>
-            <write/>
-          </privilege>
-          <privilege>
-            <write-properties/>
-          </privilege>
-          <privilege>
-            <write-content/>
-          </privilege>
-          <privilege>
-            <bind/>
-          </privilege>
-          <privilege>
-            <unbind/>
-          </privilege>
-          <privilege>
-            <unlock/>
-          </privilege>
-          <privilege>
-            <read-acl/>
-          </privilege>
-          <privilege>
-            <write-acl/>
-          </privilege>
-          <privilege>
-            <read-current-user-privilege-set/>
-          </privilege>
-          <privilege>
-            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
-          </privilege>
-        </current-user-privilege-set>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
-        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-color xmlns='http://apple.com/ns/ical/'/>
-        <calendar-order xmlns='http://apple.com/ns/ical/'/>
-        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
-        <source xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
-        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
-        <refreshrate xmlns='http://apple.com/ns/ical/'/>
-        <push-transports xmlns='http://calendarserver.org/ns/'/>
-        <pushkey xmlns='http://calendarserver.org/ns/'/>
-        <publish-url xmlns='http://calendarserver.org/ns/'/>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-</multistatus>
-"""
-
-CALENDAR_HOME_PROPFIND_RESPONSE = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {
-    "xmpp": """\
-        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>""",
-}
-
-CALENDAR_HOME_PROPFIND_RESPONSE_WITH_XMPP = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {
-    "xmpp": """\
-        <xmpp-server xmlns='http://calendarserver.org/ns/'>xmpp.example.invalid:1952</xmpp-server>
-        <xmpp-uri xmlns='http://calendarserver.org/ns/'>xmpp:pubsub.xmpp.example.invalid?pubsub;node=/CalDAV/another.example.invalid/user01/</xmpp-uri>""",
-}
-
-CALENDAR_HOME_PROPFIND_RESPONSE_XMPP_MISSING = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {"xmpp": ""}
-
-
-
-class MemoryResponse(object):
-    def __init__(self, version, code, phrase, headers, bodyProducer):
-        self.version = version
-        self.code = code
-        self.phrase = phrase
-        self.headers = headers
-        self.length = bodyProducer.length
-        self._bodyProducer = bodyProducer
-
-
-    def deliverBody(self, protocol):
-        protocol.makeConnection(self._bodyProducer)
-        d = self._bodyProducer.startProducing(ProtocolToConsumerAdapter(protocol))
-        d.addCallback(lambda ignored: protocol.connectionLost(Failure(ResponseDone())))
-
-
-
-class OS_X_10_6Mixin:
-    """
-    Mixin for L{TestCase}s for L{OS_X_10_6}.
-    """
-    def setUp(self):
-        TimezoneCache.create()
-        self.record = _DirectoryRecord(
-            u"user91", u"user91", u"User 91", u"user91 at example.org", u"user91",
-        )
-        serializePath = self.mktemp()
-        os.mkdir(serializePath)
-        self.client = OS_X_10_6(
-            None,
-            "http://127.0.0.1",
-            "/principals/users/%s/",
-            serializePath,
-            self.record,
-            None,
-        )
-
-
-    def interceptRequests(self):
-        requests = []
-        def request(*args, **kwargs):
-            result = Deferred()
-            requests.append((result, args))
-            return result
-        self.client._request = request
-        return requests
-
-
-
-class OS_X_10_6Tests(OS_X_10_6Mixin, TestCase):
-    """
-    Tests for L{OS_X_10_6}.
-    """
-    def test_parsePrincipalPROPFINDResponse(self):
-        """
-        L{Principal._parsePROPFINDResponse} accepts an XML document
-        like the one in the response to a I{PROPFIND} request for
-        I{/principals/__uids__/<uid>/} and returns a C{PropFindResult}
-        representing the data from it.
-        """
-        principals = self.client._parseMultiStatus(PRINCIPAL_PROPFIND_RESPONSE)
-        principal = principals['/principals/__uids__/user01/']
-        self.assertEquals(
-            principal.getHrefProperties(),
-            {
-                davxml.principal_collection_set: URL(path='/principals/'),
-                caldavxml.calendar_home_set: URL(path='/calendars/__uids__/user01'),
-                caldavxml.calendar_user_address_set: (
-                    URL(path='/principals/__uids__/user01/'),
-                    URL(path='/principals/users/user01/'),
-                ),
-                caldavxml.schedule_inbox_URL: URL(path='/calendars/__uids__/user01/inbox/'),
-                caldavxml.schedule_outbox_URL: URL(path='/calendars/__uids__/user01/outbox/'),
-                csxml.dropbox_home_URL: URL(path='/calendars/__uids__/user01/dropbox/'),
-                csxml.notification_URL: URL(path='/calendars/__uids__/user01/notification/'),
-                davxml.principal_URL: URL(path='/principals/__uids__/user01/'),
-            }
-        )
-        self.assertEquals(
-            principal.getTextProperties(),
-            {davxml.displayname: 'User 01'})
-
-#         self.assertEquals(
-#             principal.getSomething(),
-#             {SUPPORTED_REPORT_SET: (
-#                     '{DAV:}acl-principal-prop-set',
-#                     '{DAV:}principal-match',
-#                     '{DAV:}principal-property-search',
-#                     '{DAV:}expand-property',
-#                     )})
-
-
-    def test_extractCalendars(self):
-        """
-        L{OS_X_10_6._extractCalendars} accepts a calendar home
-        PROPFIND response body and returns a list of calendar objects
-        constructed from the data extracted from the response.
-        """
-        home = "/calendars/__uids__/user01/"
-        calendars = self.client._extractCalendars(
-            self.client._parseMultiStatus(CALENDAR_HOME_PROPFIND_RESPONSE), home)
-        calendars.sort(key=lambda cal: cal.resourceType)
-        calendar, inbox = calendars
-
-        self.assertEquals(calendar.resourceType, caldavxml.calendar)
-        self.assertEquals(calendar.name, "calendar")
-        self.assertEquals(calendar.url, "/calendars/__uids__/user01/calendar/")
-        self.assertEquals(calendar.changeToken, "c2696540-4c4c-4a31-adaf-c99630776828#3")
-
-        self.assertEquals(inbox.resourceType, caldavxml.schedule_inbox)
-        self.assertEquals(inbox.name, "inbox")
-        self.assertEquals(inbox.url, "/calendars/__uids__/user01/inbox/")
-        self.assertEquals(inbox.changeToken, "a483dab3-1391-445b-b1c3-5ae9dfc81c2f#0")
-
-        self.assertEqual({}, self.client.xmpp)
-
-
-    def test_extractCalendarsXMPP(self):
-        """
-        If there is XMPP push information in a calendar home PROPFIND response,
-        L{OS_X_10_6._extractCalendars} finds it and records it.
-        """
-        home = "/calendars/__uids__/user01/"
-        self.client._extractCalendars(
-            self.client._parseMultiStatus(CALENDAR_HOME_PROPFIND_RESPONSE_WITH_XMPP),
-            home
-        )
-        self.assertEqual({
-            home: XMPPPush(
-                "xmpp.example.invalid:1952",
-                "xmpp:pubsub.xmpp.example.invalid?pubsub;node=/CalDAV/another.example.invalid/user01/",
-                "/Some/Unique/Value"
-            )},
-            self.client.xmpp
-        )
-
-
-    def test_handleMissingXMPP(self):
-        home = "/calendars/__uids__/user01/"
-        self.client._extractCalendars(
-            self.client._parseMultiStatus(CALENDAR_HOME_PROPFIND_RESPONSE_XMPP_MISSING), home)
-        self.assertEqual({}, self.client.xmpp)
-
-
-    @inlineCallbacks
-    def test_changeEventAttendee(self):
-        """
-        OS_X_10_6.changeEventAttendee removes one attendee from an
-        existing event and appends another.
-        """
-        requests = self.interceptRequests()
-
-        vevent = Component.fromString(EVENT)
-        attendees = tuple(vevent.mainComponent().properties("ATTENDEE"))
-        old = attendees[0]
-        new = old.duplicate()
-        new.setParameter('CN', 'Some Other Guy')
-        event = Event(self.client.serializeLocation(), u'/some/calendar/1234.ics', None, vevent)
-        self.client._events[event.url] = event
-        self.client.changeEventAttendee(event.url, old, new)
-
-        _ignore_result, req = requests.pop(0)
-
-        # iCal PUTs the new VCALENDAR object.
-        _ignore_expectedResponseCode, method, url, headers, body = req
-        self.assertEquals(method, 'PUT')
-        self.assertEquals(url, 'http://127.0.0.1' + event.url)
-        self.assertIsInstance(url, str)
-        self.assertEquals(headers.getRawHeaders('content-type'), ['text/calendar'])
-
-        consumer = MemoryConsumer()
-        yield body.startProducing(consumer)
-        vevent = Component.fromString(consumer.value())
-        attendees = tuple(vevent.mainComponent().properties("ATTENDEE"))
-        self.assertEquals(len(attendees), 2)
-        self.assertEquals(attendees[0].parameterValue('CN'), 'User 01')
-        self.assertEquals(attendees[1].parameterValue('CN'), 'Some Other Guy')
-
-
-    def test_addEvent(self):
-        """
-        L{OS_X_10_6.addEvent} PUTs the event passed to it to the
-        server and updates local state to reflect its existence.
-        """
-        requests = self.interceptRequests()
-
-        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/mumble/', None)
-        self.client._calendars[calendar.url] = calendar
-
-        vcalendar = Component.fromString(EVENT)
-        d = self.client.addEvent(u'/mumble/frotz.ics', vcalendar)
-
-        result, req = requests.pop(0)
-
-        # iCal PUTs the new VCALENDAR object.
-        expectedResponseCode, method, url, headers, body = req
-        self.assertEqual(expectedResponseCode, CREATED)
-        self.assertEqual(method, 'PUT')
-        self.assertEqual(url, 'http://127.0.0.1/mumble/frotz.ics')
-        self.assertIsInstance(url, str)
-        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
-
-        consumer = MemoryConsumer()
-        finished = body.startProducing(consumer)
-        def cbFinished(ignored):
-            self.assertEqual(
-                Component.fromString(consumer.value()),
-                Component.fromString(EVENT_AND_TIMEZONE))
-        finished.addCallback(cbFinished)
-
-        def requested(ignored):
-            response = MemoryResponse(
-                ('HTTP', '1', '1'), CREATED, "Created", Headers({}),
-                StringProducer(""))
-            result.callback(response)
-        finished.addCallback(requested)
-
-        return d
-
-
-    @inlineCallbacks
-    def test_addInvite(self):
-        """
-        L{OS_X_10_6.addInvite} PUTs the event passed to it to the
-        server and updates local state to reflect its existence, but
-        it also does attendee auto-complete and free-busy checks before
-        the PUT.
-        """
-
-        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/mumble/', None)
-        self.client._calendars[calendar.url] = calendar
-
-        vcalendar = Component.fromString(EVENT_INVITE)
-
-        self.client.uuid = u'urn:uuid:user01'
-        self.client.email = u'mailto:user01 at example.com'
-        self.client.principalCollection = "/principals/"
-        self.client.outbox = "/calendars/__uids__/user01/outbox/"
-
-        @inlineCallbacks
-        def _testReport(*args, **kwargs):
-            expectedResponseCode, method, url, headers, body = args
-            self.assertEqual(expectedResponseCode, (MULTI_STATUS,))
-            self.assertEqual(method, 'REPORT')
-            self.assertEqual(url, 'http://127.0.0.1/principals/')
-            self.assertIsInstance(url, str)
-            self.assertEqual(headers.getRawHeaders('content-type'), ['text/xml'])
-
-            consumer = MemoryConsumer()
-            yield body.startProducing(consumer)
-
-            response = MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "MultiStatus", Headers({}),
-                StringProducer("<?xml version='1.0' encoding='UTF-8'?><multistatus xmlns='DAV:' />"))
-
-            returnValue(response)
-
-        @inlineCallbacks
-        def _testPost(*args, **kwargs):
-            expectedResponseCode, method, url, headers, body = args
-            self.assertEqual(expectedResponseCode, OK)
-            self.assertEqual(method, 'POST')
-            self.assertEqual(url, 'http://127.0.0.1/calendars/__uids__/user01/outbox/')
-            self.assertIsInstance(url, str)
-            self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
-
-            consumer = MemoryConsumer()
-            yield body.startProducing(consumer)
-            self.assertNotEqual(consumer.value().find(kwargs["attendee"]), -1)
-
-            response = MemoryResponse(
-                ('HTTP', '1', '1'), OK, "OK", Headers({}),
-                StringProducer(""))
-
-            returnValue(response)
-
-        def _testPost02(*args, **kwargs):
-            return _testPost(*args, attendee="ATTENDEE:mailto:user02 at example.com", **kwargs)
-
-        def _testPost03(*args, **kwargs):
-            return _testPost(*args, attendee="ATTENDEE:mailto:user03 at example.com", **kwargs)
-
-        @inlineCallbacks
-        def _testPut(*args, **kwargs):
-            expectedResponseCode, method, url, headers, body = args
-            self.assertEqual(expectedResponseCode, CREATED)
-            self.assertEqual(method, 'PUT')
-            self.assertEqual(url, 'http://127.0.0.1/mumble/frotz.ics')
-            self.assertIsInstance(url, str)
-            self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
-
-            consumer = MemoryConsumer()
-            yield body.startProducing(consumer)
-            self.assertEqual(
-                Component.fromString(consumer.value()),
-                Component.fromString(EVENT_INVITE))
-
-            response = MemoryResponse(
-                ('HTTP', '1', '1'), CREATED, "Created", Headers({}),
-                StringProducer(""))
-
-            returnValue(response)
-
-        requests = [_testReport, _testPost02, _testReport, _testPost03, _testPut, ]
-
-        def _requestHandler(*args, **kwargs):
-            handler = requests.pop(0)
-            return handler(*args, **kwargs)
-        self.client._request = _requestHandler
-        yield self.client.addInvite('/mumble/frotz.ics', vcalendar)
-
-
-    def test_deleteEvent(self):
-        """
-        L{OS_X_10_6.deleteEvent} DELETEs the event at the relative
-        URL passed to it and updates local state to reflect its
-        removal.
-        """
-        requests = self.interceptRequests()
-
-        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/foo/', None)
-        event = Event(None, calendar.url + u'bar.ics', None)
-        self.client._calendars[calendar.url] = calendar
-        self.client._setEvent(event.url, event)
-
-        d = self.client.deleteEvent(event.url)
-
-        result, req = requests.pop()
-
-        expectedResponseCode, method, url = req
-
-        self.assertEqual(expectedResponseCode, NO_CONTENT)
-        self.assertEqual(method, 'DELETE')
-        self.assertEqual(url, 'http://127.0.0.1' + event.url)
-        self.assertIsInstance(url, str)
-
-        self.assertNotIn(event.url, self.client._events)
-        self.assertNotIn(u'bar.ics', calendar.events)
-
-        response = MemoryResponse(
-            ('HTTP', '1', '1'), NO_CONTENT, "No Content", None,
-            StringProducer(""))
-        result.callback(response)
-        return d
-
-
-    def test_serialization(self):
-        """
-        L{OS_X_10_6.serialize} properly generates a JSON document.
-        """
-        clientPath = os.path.join(self.client.serializePath, "user91-OS_X_10.6")
-        self.assertFalse(os.path.exists(clientPath))
-        indexPath = os.path.join(clientPath, "index.json")
-        self.assertFalse(os.path.exists(indexPath))
-
-        cal1 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:004f8e41-b071-4b30-bb3b-6aada4adcc10
-DTSTART:20120817T113000
-DTEND:20120817T114500
-DTSTAMP:20120815T154420Z
-SEQUENCE:2
-SUMMARY:Simple event
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        cal2 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:00a79cad-857b-418e-a54a-340b5686d747
-DTSTART:20120817T113000
-DTEND:20120817T114500
-DTSTAMP:20120815T154420Z
-SEQUENCE:2
-SUMMARY:Simple event
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        events = (
-            Event(self.client.serializeLocation(), u'/home/calendar/1.ics', u'123.123', Component.fromString(cal1)),
-            Event(self.client.serializeLocation(), u'/home/inbox/i1.ics', u'123.123', Component.fromString(cal2)),
-        )
-        self.client._events.update(dict([[event.url, event] for event in events]))
-
-        calendars = (
-            Calendar(str(caldavxml.calendar), set(('VEVENT',)), u'calendar', u'/home/calendar/', "123"),
-            Calendar(str(caldavxml.calendar), set(('VTODO',)), u'tasks', u'/home/tasks/', "456"),
-            Calendar(str(caldavxml.schedule_inbox), set(('VEVENT', "VTODO",)), u'calendar', u'/home/inbox/', "789"),
-        )
-        self.client._calendars.update(dict([[calendar.url, calendar] for calendar in calendars]))
-        self.client._calendars["/home/calendar/"].events["1.ics"] = events[0]
-        self.client._calendars["/home/inbox/"].events["i1.ics"] = events[1]
-
-        self.client.serialize()
-        self.assertTrue(os.path.exists(clientPath))
-        self.assertTrue(os.path.exists(indexPath))
-        def _normDict(d):
-            return dict([(k, sorted(v, key=lambda x: x["changeToken" if k == "calendars" else "url"]) if v else None,) for k, v in d.items()])
-        self.assertEqual(_normDict(json.loads(open(indexPath).read())), _normDict(json.loads("""{
-  "calendars": [
-    {
-      "changeToken": "123",
-      "name": "calendar",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
-      "componentTypes": [
-        "VEVENT"
-      ],
-      "url": "/home/calendar/",
-      "events": [
-        "1.ics"
-      ]
-    },
-    {
-      "changeToken": "789",
-      "name": "calendar",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
-      "componentTypes": [
-        "VEVENT",
-        "VTODO"
-      ],
-      "url": "/home/inbox/",
-      "events": [
-        "i1.ics"
-      ]
-    },
-    {
-      "changeToken": "456",
-      "name": "tasks",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
-      "componentTypes": [
-        "VTODO"
-      ],
-      "url": "/home/tasks/",
-      "events": []
-    }
-  ],
-  "principalURL": null,
-  "events": [
-    {
-      "url": "/home/calendar/1.ics",
-      "scheduleTag": null,
-      "etag": "123.123",
-      "uid": "004f8e41-b071-4b30-bb3b-6aada4adcc10"
-    },
-    {
-      "url": "/home/inbox/i1.ics",
-      "scheduleTag": null,
-      "etag": "123.123",
-      "uid": "00a79cad-857b-418e-a54a-340b5686d747"
-    }
-  ]
-}""")))
-
-        event1Path = os.path.join(clientPath, "calendar", "1.ics")
-        self.assertTrue(os.path.exists(event1Path))
-        self.assertEqual(open(event1Path).read(), cal1)
-
-        event2Path = os.path.join(clientPath, "inbox", "i1.ics")
-        self.assertTrue(os.path.exists(event2Path))
-        self.assertEqual(open(event2Path).read(), cal2)
-
-
-    def test_deserialization(self):
-        """
-        L{OS_X_10_6.deserailize} properly parses a JSON document.
-        """
-
-        cal1 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:004f8e41-b071-4b30-bb3b-6aada4adcc10
-DTSTART:20120817T113000
-DTEND:20120817T114500
-DTSTAMP:20120815T154420Z
-SEQUENCE:2
-SUMMARY:Simple event
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        cal2 = """BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:00a79cad-857b-418e-a54a-340b5686d747
-DTSTART:20120817T113000
-DTEND:20120817T114500
-DTSTAMP:20120815T154420Z
-SEQUENCE:2
-SUMMARY:Simple event
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-
-        clientPath = os.path.join(self.client.serializePath, "user91-OS_X_10.6")
-        os.mkdir(clientPath)
-        indexPath = os.path.join(clientPath, "index.json")
-        open(indexPath, "w").write("""{
-  "calendars": [
-    {
-      "changeToken": "321",
-      "name": "calendar",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
-      "componentTypes": [
-        "VEVENT"
-      ],
-      "url": "/home/calendar/",
-      "events": [
-        "2.ics"
-      ]
-    },
-    {
-      "changeToken": "987",
-      "name": "calendar",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
-      "componentTypes": [
-        "VEVENT",
-        "VTODO"
-      ],
-      "url": "/home/inbox/",
-      "events": [
-        "i2.ics"
-      ]
-    },
-    {
-      "changeToken": "654",
-      "name": "tasks",
-      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
-      "componentTypes": [
-        "VTODO"
-      ],
-      "url": "/home/tasks/",
-      "events": []
-    }
-  ],
-  "principalURL": null,
-  "events": [
-    {
-      "url": "/home/calendar/2.ics",
-      "scheduleTag": null,
-      "etag": "321.321",
-      "uid": "004f8e41-b071-4b30-bb3b-6aada4adcc10"
-    },
-    {
-      "url": "/home/inbox/i2.ics",
-      "scheduleTag": null,
-      "etag": "987.987",
-      "uid": "00a79cad-857b-418e-a54a-340b5686d747"
-    }
-  ]
-}""")
-
-        os.mkdir(os.path.join(clientPath, "calendar"))
-        event1Path = os.path.join(clientPath, "calendar", "2.ics")
-        open(event1Path, "w").write(cal1)
-        os.mkdir(os.path.join(clientPath, "inbox"))
-        event1Path = os.path.join(clientPath, "inbox", "i2.ics")
-        open(event1Path, "w").write(cal2)
-
-        self.client.deserialize()
-
-        self.assertEqual(len(self.client._calendars), 3)
-        self.assertTrue("/home/calendar/" in self.client._calendars)
-        self.assertEqual(self.client._calendars["/home/calendar/"].changeToken, "321")
-        self.assertEqual(self.client._calendars["/home/calendar/"].name, "calendar")
-        self.assertEqual(self.client._calendars["/home/calendar/"].resourceType, "{urn:ietf:params:xml:ns:caldav}calendar")
-        self.assertEqual(self.client._calendars["/home/calendar/"].componentTypes, set(("VEVENT",)))
-        self.assertTrue("/home/tasks/" in self.client._calendars)
-        self.assertTrue("/home/inbox/" in self.client._calendars)
-        self.assertEqual(self.client._calendars["/home/inbox/"].componentTypes, set(("VEVENT", "VTODO",)))
-        self.assertEqual(len(self.client._events), 2)
-        self.assertTrue("/home/calendar/2.ics" in self.client._events)
-        self.assertEqual(self.client._events["/home/calendar/2.ics"].scheduleTag, None)
-        self.assertEqual(self.client._events["/home/calendar/2.ics"].etag, "321.321")
-        self.assertEqual(self.client._events["/home/calendar/2.ics"].getUID(), "004f8e41-b071-4b30-bb3b-6aada4adcc10")
-        self.assertEqual(str(self.client._events["/home/calendar/2.ics"].component), cal1)
-        self.assertTrue("/home/inbox/i2.ics" in self.client._events)
-        self.assertEqual(self.client._events["/home/inbox/i2.ics"].scheduleTag, None)
-        self.assertEqual(self.client._events["/home/inbox/i2.ics"].etag, "987.987")
-        self.assertEqual(self.client._events["/home/inbox/i2.ics"].getUID(), "00a79cad-857b-418e-a54a-340b5686d747")
-        self.assertEqual(str(self.client._events["/home/inbox/i2.ics"].component), cal2)
-
-
-
-class UpdateCalendarTests(OS_X_10_6Mixin, TestCase):
-    """
-    Tests for L{OS_X_10_6._updateCalendar}.
-    """
-
-    _CALENDAR_PROPFIND_RESPONSE_BODY = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/something/anotherthing.ics</href>
-    <propstat>
-      <prop>
-        <resourcetype>
-          <collection/>
-        </resourcetype>
-        <getetag>"None"</getetag>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-    <propstat>
-      <prop>
-      </prop>
-      <status>HTTP/1.1 404 Not Found</status>
-    </propstat>
-  </response>
-  <response>
-    <href>/something/else.ics</href>
-    <propstat>
-      <prop>
-        <resourcetype>
-          <collection/>
-        </resourcetype>
-        <getetag>"None"</getetag>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-   </response>
-</multistatus>
-"""
-    _CALENDAR_REPORT_RESPONSE_BODY = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/something/anotherthing.ics</href>
-    <status>HTTP/1.1 404 Not Found</status>
-  </response>
-  <response>
-    <href>/something/else.ics</href>
-    <propstat>
-      <prop>
-        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
-        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:CD54161A13AA8A4649D3781E at caldav.corp.apple.com
-DTSTART:20110715T140000Z
-DURATION:PT1H
-DTSTAMP:20110715T144217Z
-SUMMARY:Test2
-END:VEVENT
-END:VCALENDAR
-]]></calendar-data>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-  </response>
-</multistatus>
-"""
-
-    _CALENDAR_REPORT_RESPONSE_BODY_1 = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/something/anotherthing.ics</href>
-    <propstat>
-      <prop>
-        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
-        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:anotherthing at caldav.corp.apple.com
-DTSTART:20110715T140000Z
-DURATION:PT1H
-DTSTAMP:20110715T144217Z
-SUMMARY:Test1
-END:VEVENT
-END:VCALENDAR
-]]></calendar-data>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-  </response>
-</multistatus>
-"""
-
-    _CALENDAR_REPORT_RESPONSE_BODY_2 = """\
-<?xml version='1.0' encoding='UTF-8'?>
-<multistatus xmlns='DAV:'>
-  <response>
-    <href>/something/else.ics</href>
-    <propstat>
-      <prop>
-        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
-        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VEVENT
-UID:else at caldav.corp.apple.com
-DTSTART:20110715T140000Z
-DURATION:PT1H
-DTSTAMP:20110715T144217Z
-SUMMARY:Test2
-END:VEVENT
-END:VCALENDAR
-]]></calendar-data>
-      </prop>
-      <status>HTTP/1.1 200 OK</status>
-    </propstat>
-  </response>
-</multistatus>
-"""
-
-    def test_eventMissing(self):
-        """
-        If an event included in the calendar PROPFIND response no longer exists
-        by the time a REPORT is issued for that event, the 404 is handled and
-        the rest of the normal update logic for that event is skipped.
-        """
-        requests = self.interceptRequests()
-
-        calendar = Calendar(None, set(('VEVENT',)), 'calendar', '/something/', None)
-        self.client._calendars[calendar.url] = calendar
-        self.client._updateCalendar(calendar, "1234")
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
-        self.assertEqual('PROPFIND', method)
-        self.assertEqual('http://127.0.0.1/something/', url)
-        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
-
-        result.callback(
-            MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
-                StringProducer(self._CALENDAR_PROPFIND_RESPONSE_BODY)))
-
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
-        self.assertEqual('REPORT', method)
-        self.assertEqual('http://127.0.0.1/something/', url)
-        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
-
-        # Someone else comes along and gets rid of the event
-        del self.client._events["/something/anotherthing.ics"]
-
-        result.callback(
-            MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
-                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY)))
-
-        # Verify that processing proceeded to the response after the one with a
-        # 404 status.
-        self.assertIn('/something/else.ics', self.client._events)
-
-
-    def test_multigetBatch(self):
-        """
-        If an event included in the calendar PROPFIND response no longer exists
-        by the time a REPORT is issued for that event, the 404 is handled and
-        the rest of the normal update logic for that event is skipped.
-        """
-        requests = self.interceptRequests()
-
-        self.patch(self.client, "MULTIGET_BATCH_SIZE", 1)
-
-        calendar = Calendar(None, set(('VEVENT',)), 'calendar', '/something/', None)
-        self.client._calendars[calendar.url] = calendar
-        self.client._updateCalendar(calendar, "1234")
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
-        self.assertEqual('PROPFIND', method)
-        self.assertEqual('http://127.0.0.1/something/', url)
-        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
-
-        result.callback(
-            MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
-                StringProducer(self._CALENDAR_PROPFIND_RESPONSE_BODY)))
-
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
-        self.assertEqual('REPORT', method)
-        self.assertEqual('http://127.0.0.1/something/', url)
-        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
-
-        result.callback(
-            MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
-                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY_1)))
-
-        self.assertTrue(self.client._events['/something/anotherthing.ics'].etag is not None)
-        self.assertTrue(self.client._events['/something/else.ics'].etag is None)
-
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
-        self.assertEqual('REPORT', method)
-        self.assertEqual('http://127.0.0.1/something/', url)
-        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
-
-        result.callback(
-            MemoryResponse(
-                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
-                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY_2)))
-
-        self.assertTrue(self.client._events['/something/anotherthing.ics'].etag is not None)
-        self.assertTrue(self.client._events['/something/else.ics'].etag is not None)
-
-
-
-class VFreeBusyTests(OS_X_10_6Mixin, TestCase):
-    """
-    Tests for L{OS_X_10_6.requestAvailability}.
-    """
-    def test_requestAvailability(self):
-        """
-        L{OS_X_10_6.requestAvailability} accepts a date range and a set of
-        account uuids and issues a VFREEBUSY request.  It returns a Deferred
-        which fires with a dict mapping account uuids to availability range
-        information.
-        """
-        self.client.uuid = u'urn:uuid:user01'
-        self.client.email = u'mailto:user01 at example.com'
-        self.client.outbox = "/calendars/__uids__/%s/outbox/" % (self.record.uid,)
-        requests = self.interceptRequests()
-
-        start = DateTime(2011, 6, 10, 10, 45, 0, tzid=Timezone.UTCTimezone)
-        end = DateTime(2011, 6, 10, 11, 15, 0, tzid=Timezone.UTCTimezone)
-        d = self.client.requestAvailability(
-            start, end, [u"urn:uuid:user05", u"urn:uuid:user10"])
-
-        result, req = requests.pop(0)
-        expectedResponseCode, method, url, headers, body = req
-
-        self.assertEqual(OK, expectedResponseCode)
-        self.assertEqual('POST', method)
-        self.assertEqual(
-            'http://127.0.0.1/calendars/__uids__/%s/outbox/' % (self.record.uid,),
-            url)
-
-        self.assertEqual(headers.getRawHeaders('originator'), ['mailto:user01 at example.com'])
-        self.assertEqual(headers.getRawHeaders('recipient'), ['urn:uuid:user05, urn:uuid:user10'])
-        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
-
-        consumer = MemoryConsumer()
-        finished = body.startProducing(consumer)
-        def cbFinished(ignored):
-            vevent = Component.fromString(consumer.value())
-            uid = vevent.resourceUID()
-            dtstamp = vevent.mainComponent().propertyValue("DTSTAMP")
-            dtstamp = dtstamp.getText()
-            self.assertEqual("""BEGIN:VCALENDAR
-CALSCALE:GREGORIAN
-VERSION:2.0
-METHOD:REQUEST
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-BEGIN:VFREEBUSY
-UID:%(uid)s
-DTEND:20110611T000000Z
-ATTENDEE:urn:uuid:user05
-ATTENDEE:urn:uuid:user10
-DTSTART:20110610T000000Z
-DTSTAMP:%(dtstamp)s
-ORGANIZER:mailto:user01 at example.com
-SUMMARY:Availability for urn:uuid:user05, urn:uuid:user10
-END:VFREEBUSY
-END:VCALENDAR
-""".replace('\n', '\r\n') % {'uid': uid, 'dtstamp': dtstamp}, consumer.value())
-
-        finished.addCallback(cbFinished)
-
-        def requested(ignored):
-            response = MemoryResponse(
-                ('HTTP', '1', '1'), OK, "Ok", Headers({}),
-                StringProducer(""))
-            result.callback(response)
-        finished.addCallback(requested)
-
-        return d

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_population.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_population.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_population.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,390 +0,0 @@
-##
-# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-"""
-Tests for some things in L{loadtest.population}.
-"""
-
-from twisted.trial.unittest import TestCase
-
-from contrib.performance.loadtest.population import ReportStatistics
-
-class ReportStatisticsTests(TestCase):
-    """
-    Tests for L{loadtest.population.ReportStatistics}.
-    """
-    def test_countUsers(self):
-        """
-        L{ReportStatistics.countUsers} returns the number of users observed to
-        have acted in the simulation.
-        """
-        logger = ReportStatistics()
-        users = ['user01', 'user02', 'user03']
-        for user in users:
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=1.23, user=user, client_type="test", client_id="1234"
-            ))
-        self.assertEqual(len(users), logger.countUsers())
-
-
-    def test_countClients(self):
-        """
-        L{ReportStatistics.countClients} returns the number of clients observed to
-        have acted in the simulation.
-        """
-        logger = ReportStatistics()
-        clients = ['c01', 'c02', 'c03']
-        for client in clients:
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=1.23, user="user01", client_type="test", client_id=client
-            ))
-        self.assertEqual(len(clients), logger.countClients())
-
-
-    def test_clientFailures(self):
-        """
-        L{ReportStatistics.countClientFailures} returns the number of clients observed to
-        have failed in the simulation.
-        """
-        logger = ReportStatistics()
-        clients = ['c01', 'c02', 'c03']
-        for client in clients:
-            logger.observe(dict(
-                type='client-failure', reason="testing %s" % (client,)
-            ))
-        self.assertEqual(len(clients), logger.countClientFailures())
-
-
-    def test_simFailures(self):
-        """
-        L{ReportStatistics.countSimFailures} returns the number of clients observed to
-        have caused an error in the simulation.
-        """
-        logger = ReportStatistics()
-        clients = ['c01', 'c02', 'c03']
-        for client in clients:
-            logger.observe(dict(
-                type='sim-failure', reason="testing %s" % (client,)
-            ))
-        self.assertEqual(len(clients), logger.countSimFailures())
-
-
-    def test_noFailures(self):
-        """
-        If fewer than 1% of requests fail, fewer than 1% of requests take 5
-        seconds or more, and fewer than 5% of requests take 3 seconds or more,
-        L{ReportStatistics.failures} returns an empty list.
-        """
-        logger = ReportStatistics()
-        logger.observe(dict(
-            type='response', method='GET', success=True,
-            duration=2.5, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual([], logger.failures())
-
-
-    def test_requestFailures(self):
-        """
-        If more than 1% of requests fail, L{ReportStatistics.failures} returns a
-        list containing a string describing this.
-        """
-        logger = ReportStatistics()
-        for _ignore in range(98):
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=2.5, user='user01', client_type="test", client_id="1234"
-            ))
-        logger.observe(dict(
-            type='response', method='GET', success=False,
-            duration=2.5, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 1% GET failed"],
-            logger.failures())
-
-
-    def test_threeSecondFailure(self):
-        """
-        If more than 5% of requests take longer than 3 seconds,
-        L{ReportStatistics.failures} returns a list containing a string
-        describing that.
-        """
-        logger = ReportStatistics()
-        for _ignore in range(94):
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=2.5, user='user01', client_type="test", client_id="1234"
-            ))
-        for _ignore in range(5):
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=3.5, user='user02', client_type="test", client_id="1234"
-            ))
-        self.assertEqual(
-            ["Greater than 5% GET exceeded 3 second response time"],
-            logger.failures())
-
-
-    def test_fiveSecondFailure(self):
-        """
-        If more than 1% of requests take longer than 5 seconds,
-        L{ReportStatistics.failures} returns a list containing a string
-        describing that.
-        """
-        logger = ReportStatistics()
-        for _ignore in range(98):
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=2.5, user='user01', client_type="test", client_id="1234"
-            ))
-        logger.observe(dict(
-            type='response', method='GET', success=True,
-            duration=5.5, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 1% GET exceeded 5 second response time"],
-            logger.failures())
-
-
-    def test_methodsCountedSeparately(self):
-        """
-        The counts for one method do not affect the results of another method.
-        """
-        logger = ReportStatistics()
-        for _ignore in range(99):
-            logger.observe(dict(
-                type='response', method='GET', success=True,
-                duration=2.5, user='user01', client_type="test", client_id="1234"
-            ))
-            logger.observe(dict(
-                type='response', method='POST', success=True,
-                duration=2.5, user='user01', client_type="test", client_id="1234"
-            ))
-
-        logger.observe(dict(
-            type='response', method='GET', success=False,
-            duration=2.5, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='POST', success=False,
-            duration=2.5, user='user01', client_type="test", client_id="1234"
-        ))
-
-        self.assertEqual([], logger.failures())
-
-
-    def test_bucketRequest(self):
-        """
-        PUT(xxx-huge/large/medium/small} have different thresholds. Test that requests straddling
-        each of those are correctly determined to be failures or not.
-        """
-
-        _thresholds = {
-            "requests": {
-                "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
-                "thresholds": {
-                    "default": [100.0, 100.0, 100.0, 5.0, 1.0, 0.5, 0.0],
-                    "PUT{organizer-small}": [100.0, 50.0, 25.0, 5.0, 1.0, 0.5, 0.0],
-                    "PUT{organizer-medium}": [100.0, 100.0, 50.0, 25.0, 5.0, 1.0, 0.5],
-                    "PUT{organizer-large}": [100.0, 100.0, 100.0, 50.0, 25.0, 5.0, 1.0],
-                    "PUT{organizer-huge}": [100.0, 100.0, 100.0, 100.0, 100.0, 50.0, 25.0],
-                }
-            }
-        }
-
-        # -small below threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual([], logger.failures())
-
-        # -small above 0.5 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-small}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 50% PUT{organizer-small} exceeded 0.5 second response time"],
-            logger.failures()
-        )
-
-        # -medium below 0.5 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=0.6, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            [],
-            logger.failures()
-        )
-
-        # -medium above 1.0 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-medium}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 50% PUT{organizer-medium} exceeded 1 second response time"],
-            logger.failures()
-        )
-
-        # -large below 1.0 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=1.6, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            [],
-            logger.failures()
-        )
-
-        # -large above 3.0 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=0.2, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=3.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=3.6, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-large}', success=True,
-            duration=3.6, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 50% PUT{organizer-large} exceeded 3 second response time"],
-            logger.failures()
-        )
-
-        # -huge below 10.0 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=12.0, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=8, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=11.0, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=9.0, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            [],
-            logger.failures()
-        )
-
-        # -huge above 10.0 threshold
-        logger = ReportStatistics(thresholds=_thresholds)
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=12.0, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=9.0, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=12.0, user='user01', client_type="test", client_id="1234"
-        ))
-        logger.observe(dict(
-            type='response', method='PUT{organizer-huge}', success=True,
-            duration=42.42, user='user01', client_type="test", client_id="1234"
-        ))
-        self.assertEqual(
-            ["Greater than 50% PUT{organizer-huge} exceeded 10 second response time"],
-            logger.failures()
-        )

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_profiles.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_profiles.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_profiles.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,1091 +0,0 @@
-##
-# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-"""
-Tests for loadtest.profiles.
-"""
-
-from StringIO import StringIO
-
-from caldavclientlibrary.protocol.caldav.definitions import caldavxml, csxml
-
-from twisted.trial.unittest import TestCase
-from twisted.internet.task import Clock
-from twisted.internet.defer import succeed, fail
-from twisted.web.http import NO_CONTENT, PRECONDITION_FAILED
-from twisted.web.client import Response
-
-from twistedcaldav.ical import Component, Property
-
-from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter, OperationLogger
-from contrib.performance.loadtest.profiles import RealisticInviter
-from contrib.performance.loadtest.population import Populator, CalendarClientSimulator
-from contrib.performance.loadtest.ical import IncorrectResponseCode, Calendar, Event, BaseClient
-from contrib.performance.loadtest.sim import _DirectoryRecord
-
-import os
-
-SIMPLE_EVENT = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Apple Inc.//iCal 4.0.3//EN
-CALSCALE:GREGORIAN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-BEGIN:DAYLIGHT
-TZOFFSETFROM:-0500
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-DTSTART:20070311T020000
-TZNAME:EDT
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-TZOFFSETFROM:-0400
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-DTSTART:20071104T020000
-TZNAME:EST
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-CREATED:20101018T155431Z
-UID:C98AD237-55AD-4F7D-9009-0D355D835822
-DTEND;TZID=America/New_York:20101021T130000
-TRANSP:OPAQUE
-SUMMARY:Simple event
-DTSTART;TZID=America/New_York:20101021T120000
-DTSTAMP:20101018T155438Z
-SEQUENCE:2
-END:VEVENT
-END:VCALENDAR
-"""
-
-INVITED_EVENT = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
-DTSTART;TZID=America/New_York:20110131T130000
-DTEND;TZID=America/New_York:20110131T140000
-ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
- CEPTED:urn:uuid:user01
-ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=NE
- EDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:user02
-CREATED:20110124T170357Z
-DTSTAMP:20110124T170425Z
-ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
-SEQUENCE:3
-SUMMARY:Some Event For You
-TRANSP:TRANSPARENT
-X-APPLE-NEEDS-REPLY:TRUE
-END:VEVENT
-END:VCALENDAR
-"""
-
-ACCEPTED_EVENT = """\
-BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-TZID:America/New_York
-BEGIN:STANDARD
-DTSTART:20071104T020000
-RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-BEGIN:DAYLIGHT
-DTSTART:20070311T020000
-RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
-DTSTART;TZID=America/New_York:20110131T130000
-DTEND;TZID=America/New_York:20110131T140000
-ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
- CEPTED:urn:uuid:user01
-ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=AC
- CEPTED:urn:uuid:user02
-CREATED:20110124T170357Z
-DTSTAMP:20110124T170425Z
-ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
-SEQUENCE:3
-SUMMARY:Some Event For You
-TRANSP:TRANSPARENT
-X-APPLE-NEEDS-REPLY:TRUE
-END:VEVENT
-END:VCALENDAR
-"""
-
-INBOX_REPLY = """\
-BEGIN:VCALENDAR
-METHOD:REPLY
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-"""
-
-
-
-class AnyUser(object):
-    def __getitem__(self, index):
-        return _AnyRecord(index)
-
-
-
-class _AnyRecord(object):
-    def __init__(self, index):
-        self.uid = u"user%02d" % (index,)
-        self.password = u"user%02d" % (index,)
-        self.commonName = u"User %02d" % (index,)
-        self.email = u"user%02d at example.com" % (index,)
-        self.guid = u"user%02d" % (index,)
-
-
-
-class Deterministic(object):
-    def __init__(self, value=None):
-        self.value = value
-
-
-    def gauss(self, mean, stddev):
-        """
-        Pretend to return a value from a gaussian distribution with mu
-        parameter C{mean} and sigma parameter C{stddev}.  But actually
-        always return C{mean + 1}.
-        """
-        return mean + 1
-
-
-    def choice(self, sequence):
-        return sequence[0]
-
-
-    def sample(self):
-        return self.value
-
-
-
-class StubClient(BaseClient):
-    """
-    Stand in for an iCalendar client.
-
-    @ivar rescheduled: A set of event URLs which will not allow
-        attendee changes due to a changed schedule tag.
-    @ivar _pendingFailures: dict mapping URLs to failure objects
-    """
-    def __init__(self, number, serializePath):
-        self.serializePath = serializePath
-        os.mkdir(self.serializePath)
-        self.title = "StubClient"
-        self._events = {}
-        self._calendars = {}
-        self._pendingFailures = {}
-        self.record = _DirectoryRecord(
-            "user%02d" % (number,), "user%02d" % (number,),
-            "User %02d" % (number,), "user%02d at example.org" % (number,),
-            "user%02d" % (number,))
-        self.email = "mailto:user%02d at example.com" % (number,)
-        self.uuid = "urn:uuid:user%02d" % (number,)
-        self.rescheduled = set()
-        self.started = True
-
-
-    def _failDeleteWithObject(self, href, failureObject):
-        """
-        Accessor for inserting intentional failures for deletes.
-        """
-        self._pendingFailures[href] = failureObject
-
-
-    def serializeLocation(self):
-        """
-        Return the path to the directory where data for this user is serialized.
-        """
-        if self.serializePath is None or not os.path.isdir(self.serializePath):
-            return None
-
-        key = "%s-%s" % (self.record.uid, "StubClient")
-        path = os.path.join(self.serializePath, key)
-        if not os.path.exists(path):
-            os.mkdir(path)
-        elif not os.path.isdir(path):
-            return None
-
-        return path
-
-
-    def addEvent(self, href, vevent):
-        self._events[href] = Event(self.serializePath, href, None, vevent)
-        return succeed(None)
-
-
-    def addInvite(self, href, vevent):
-        return self.addEvent(href, vevent)
-
-
-    def deleteEvent(self, href,):
-        del self._events[href]
-        calendar, uid = href.rsplit('/', 1)
-        del self._calendars[calendar + '/'].events[uid]
-        if href in self._pendingFailures:
-            failureObject = self._pendingFailures.pop(href)
-            return fail(failureObject)
-        else:
-            return succeed(None)
-
-
-    def updateEvent(self, href):
-        self.rescheduled.remove(href)
-        return succeed(None)
-
-
-    def addEventAttendee(self, href, attendee):
-        vevent = self._events[href].component
-        vevent.mainComponent().addProperty(attendee)
-        self._events[href].component = vevent
-
-
-    def changeEventAttendee(self, href, old, new):
-        if href in self.rescheduled:
-            return fail(IncorrectResponseCode(
-                NO_CONTENT,
-                Response(
-                    ('HTTP', 1, 1), PRECONDITION_FAILED,
-                    'Precondition Failed', None, None))
-            )
-
-        vevent = self._events[href].component
-        vevent.mainComponent().removeProperty(old)
-        vevent.mainComponent().addProperty(new)
-        self._events[href].component = vevent
-        return succeed(None)
-
-
-    def _makeSelfAttendee(self):
-        attendee = Property(
-            name=u'ATTENDEE',
-            value=self.email,
-            params={
-                'CN': self.record.commonName,
-                'CUTYPE': 'INDIVIDUAL',
-                'PARTSTAT': 'ACCEPTED',
-            },
-        )
-        return attendee
-
-
-    def _makeSelfOrganizer(self):
-        organizer = Property(
-            name=u'ORGANIZER',
-            value=self.email,
-            params={
-                'CN': self.record.commonName,
-            },
-        )
-        return organizer
-
-
-
-class SequentialDistribution(object):
-    def __init__(self, values):
-        self.values = values
-
-
-    def sample(self):
-        return self.values.pop(0)
-
-
-
-class InviterTests(TestCase):
-    """
-    Tests for loadtest.profiles.Inviter.
-    """
-    def setUp(self):
-        self.sim = CalendarClientSimulator(
-            AnyUser(), Populator(None), None, None, None, None, None)
-
-
-    def _simpleAccount(self, userNumber, eventText):
-        client = StubClient(userNumber, self.mktemp())
-
-        vevent = Component.fromString(eventText)
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/cal/', None)
-        client._calendars.update({calendar.url: calendar})
-
-        event = Event(client.serializeLocation(), calendar.url + u'1234.ics', None, vevent)
-
-        client._events.update({event.url: event})
-        calendar.events = {u'1234.ics': event}
-
-        return vevent, event, calendar, client
-
-
-    def test_enabled(self):
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-
-        inviter = Inviter(None, self.sim, client, userNumber, **{"enabled": False})
-        self.assertEqual(inviter.enabled, False)
-
-        inviter = Inviter(None, self.sim, client, userNumber, **{"enabled": True})
-        self.assertEqual(inviter.enabled, True)
-
-
-    def test_doNotAddAttendeeToInbox(self):
-        """
-        When the only calendar with any events is a schedule inbox, no
-        attempt is made to add attendees to an event on that calendar.
-        """
-        userNumber = 10
-        vevent, _ignore_event, calendar, client = self._simpleAccount(
-            userNumber, SIMPLE_EVENT)
-        calendar.resourceType = caldavxml.schedule_inbox
-        inviter = Inviter(None, self.sim, client, userNumber)
-        inviter._invite()
-        self.assertFalse(vevent.mainComponent().hasProperty('ATTENDEE'))
-
-
-    def test_doNotAddAttendeeToNoCalendars(self):
-        """
-        When there are no calendars and no events at all, the inviter
-        does nothing.
-        """
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-        inviter = Inviter(None, self.sim, client, userNumber)
-        inviter._invite()
-        self.assertEquals(client._events, {})
-        self.assertEquals(client._calendars, {})
-
-
-    def test_doNotAddAttendeeToUninitializedEvent(self):
-        """
-        When there is an L{Event} on a calendar but the details of the
-        event have not yet been retrieved, no attempt is made to add
-        invitees to that event.
-        """
-        userNumber = 19
-        _ignore_vevent, event, calendar, client = self._simpleAccount(
-            userNumber, SIMPLE_EVENT)
-        event.component = event.etag = event.scheduleTag = None
-        inviter = Inviter(None, self.sim, client, userNumber)
-        inviter._invite()
-        self.assertEquals(client._events, {event.url: event})
-        self.assertEquals(client._calendars, {calendar.url: calendar})
-
-
-    def test_addAttendeeToEvent(self):
-        """
-        When there is a normal calendar with an event, inviter adds an
-        attendee to it.
-        """
-        userNumber = 16
-        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
-            userNumber, SIMPLE_EVENT)
-        inviter = Inviter(Clock(), self.sim, client, userNumber)
-        inviter.setParameters(inviteeDistribution=Deterministic(1))
-        inviter._invite()
-        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 1)
-        for paramname, paramvalue in {
-            'CN': 'User %d' % (userNumber + 1,),
-            'CUTYPE': 'INDIVIDUAL',
-            'PARTSTAT': 'NEEDS-ACTION',
-            'ROLE': 'REQ-PARTICIPANT',
-            'RSVP': 'TRUE'
-        }.items():
-            self.assertTrue(attendees[0].hasParameter(paramname))
-            self.assertEqual(attendees[0].parameterValue(paramname), paramvalue)
-
-
-    def test_doNotAddSelfToEvent(self):
-        """
-        If the inviter randomly selects its own user to be added to
-        the attendee list, a different user is added instead.
-        """
-        selfNumber = 12
-        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
-            selfNumber, SIMPLE_EVENT)
-
-        otherNumber = 20
-        values = [selfNumber - selfNumber, otherNumber - selfNumber]
-
-        inviter = Inviter(Clock(), self.sim, client, selfNumber)
-        inviter.setParameters(inviteeDistribution=SequentialDistribution(values))
-        inviter._invite()
-        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 1)
-        for paramname, paramvalue in {
-            'CN': 'User %d' % (otherNumber,),
-            'CUTYPE': 'INDIVIDUAL',
-            'PARTSTAT': 'NEEDS-ACTION',
-            'ROLE': 'REQ-PARTICIPANT',
-            'RSVP': 'TRUE'
-        }.items():
-            self.assertTrue(attendees[0].hasParameter(paramname))
-            self.assertEqual(attendees[0].parameterValue(paramname), paramvalue)
-
-
-    def test_doNotAddExistingToEvent(self):
-        """
-        If the inviter randomly selects a user which is already an
-        invitee on the event, a different user is added instead.
-        """
-        selfNumber = 1
-        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
-            selfNumber, INVITED_EVENT)
-
-        invitee = tuple(event.component.mainComponent().properties('ATTENDEE'))[0]
-        inviteeNumber = int(invitee.parameterValue('CN').split()[1])
-        anotherNumber = inviteeNumber + 5
-        values = [inviteeNumber - selfNumber, anotherNumber - selfNumber]
-
-        inviter = Inviter(Clock(), self.sim, client, selfNumber)
-        inviter.setParameters(inviteeDistribution=SequentialDistribution(values))
-        inviter._invite()
-        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 3)
-        for paramname, paramvalue in {
-            'CN': 'User %02d' % (anotherNumber,),
-            'CUTYPE': 'INDIVIDUAL',
-            'PARTSTAT': 'NEEDS-ACTION',
-            'ROLE': 'REQ-PARTICIPANT',
-            'RSVP': 'TRUE'
-        }.items():
-            self.assertTrue(attendees[2].hasParameter(paramname))
-            self.assertEqual(attendees[2].parameterValue(paramname), paramvalue)
-
-
-    def test_everybodyInvitedAlready(self):
-        """
-        If the first so-many randomly selected users we come across
-        are already attendees on the event, the invitation attempt is
-        abandoned.
-        """
-        selfNumber = 1
-        vevent, _ignore_event, _ignore_calendar, client = self._simpleAccount(
-            selfNumber, INVITED_EVENT)
-        inviter = Inviter(Clock(), self.sim, client, selfNumber)
-        # Always return a user number which has already been invited.
-        inviter.setParameters(inviteeDistribution=Deterministic(2 - selfNumber))
-        inviter._invite()
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 2)
-
-
-    def test_doNotInviteToSomeoneElsesEvent(self):
-        """
-        If there are events on our calendar which are being organized
-        by someone else, the inviter does not attempt to invite new
-        users to them.
-        """
-        selfNumber = 2
-        vevent, _ignore_event, _ignore_calendar, client = self._simpleAccount(
-            selfNumber, INVITED_EVENT)
-        inviter = Inviter(None, self.sim, client, selfNumber)
-        # Try to send an invitation, but with only one event on the
-        # calendar, of which we are not the organizer.  It should be
-        # unchanged afterwards.
-        inviter._invite()
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        self.assertEqual(len(attendees), 2)
-        self.assertEqual(attendees[0].parameterValue('CN'), 'User 01')
-        self.assertEqual(attendees[1].parameterValue('CN'), 'User 02')
-
-
-
-class RealisticInviterTests(TestCase):
-    """
-    Tests for loadtest.profiles.RealisticInviter.
-    """
-    def setUp(self):
-        self.sim = CalendarClientSimulator(
-            AnyUser(), Populator(None), None, None, None, None, None)
-
-
-    def _simpleAccount(self, userNumber, eventText):
-        client = StubClient(userNumber, self.mktemp())
-        vevent = Component.fromString(eventText)
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/cal/', None)
-        event = Event(client.serializeLocation(), calendar.url + u'1234.ics', None, vevent)
-        calendar.events = {u'1234.ics': event}
-        client._events.update({event.url: event})
-        client._calendars.update({calendar.url: calendar})
-
-        return vevent, event, calendar, client
-
-
-    def test_enabled(self):
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-
-        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": False})
-        self.assertEqual(inviter.enabled, False)
-
-        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": True})
-        self.assertEqual(inviter.enabled, True)
-
-
-    def test_doNotAddInviteToInbox(self):
-        """
-        When the only calendar with any events is a schedule inbox, no
-        attempt is made to add attendees to that calendar.
-        """
-        calendar = Calendar(
-            caldavxml.schedule_inbox, set(), u'inbox', u'/sched/inbox', None)
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-
-        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": False})
-        inviter._invite()
-
-        self.assertEquals(client._events, {})
-
-
-    def test_doNotAddInviteToNoCalendars(self):
-        """
-        When there are no calendars and no events at all, the inviter
-        does nothing.
-        """
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-        inviter = RealisticInviter(None, self.sim, client, userNumber)
-        inviter._invite()
-        self.assertEquals(client._events, {})
-        self.assertEquals(client._calendars, {})
-
-
-    def test_addInvite(self):
-        """
-        When there is a normal calendar, inviter adds an invite to it.
-        """
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
-        userNumber = 16
-        serializePath = self.mktemp()
-        os.mkdir(serializePath)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-        inviter = RealisticInviter(Clock(), self.sim, client, userNumber)
-        inviter.setParameters(
-            inviteeDistribution=Deterministic(1),
-            inviteeCountDistribution=Deterministic(1)
-        )
-        inviter._invite()
-        self.assertEquals(len(client._events), 1)
-        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
-        expected = set(("mailto:user%02d at example.com" % (userNumber,), "mailto:user%02d at example.com" % (userNumber + 1,),))
-        for attendee in attendees:
-            expected.remove(attendee.value())
-        self.assertEqual(len(expected), 0)
-
-
-    def test_doNotAddSelfToEvent(self):
-        """
-        If the inviter randomly selects its own user to be added to
-        the attendee list, a different user is added instead.
-        """
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
-        selfNumber = 12
-        client = StubClient(selfNumber, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-
-        otherNumber = 20
-        values = [selfNumber - selfNumber, otherNumber - selfNumber]
-
-        inviter = RealisticInviter(Clock(), self.sim, client, selfNumber)
-        inviter.setParameters(
-            inviteeDistribution=SequentialDistribution(values),
-            inviteeCountDistribution=Deterministic(1)
-        )
-        inviter._invite()
-        self.assertEquals(len(client._events), 1)
-        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
-        expected = set(("mailto:user%02d at example.com" % (selfNumber,), "mailto:user%02d at example.com" % (otherNumber,),))
-        for attendee in attendees:
-            expected.remove(attendee.value())
-        self.assertEqual(len(expected), 0)
-
-
-    def test_doNotAddExistingToEvent(self):
-        """
-        If the inviter randomly selects a user which is already an
-        invitee on the event, a different user is added instead.
-        """
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
-        selfNumber = 1
-        client = StubClient(selfNumber, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-
-        inviteeNumber = 20
-        anotherNumber = inviteeNumber + 5
-        values = [inviteeNumber - selfNumber, inviteeNumber - selfNumber, anotherNumber - selfNumber]
-
-        inviter = RealisticInviter(Clock(), self.sim, client, selfNumber)
-        inviter.setParameters(
-            inviteeDistribution=SequentialDistribution(values),
-            inviteeCountDistribution=Deterministic(2)
-        )
-        inviter._invite()
-        self.assertEquals(len(client._events), 1)
-        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
-        expected = set((
-            "mailto:user%02d at example.com" % (selfNumber,),
-            "mailto:user%02d at example.com" % (inviteeNumber,),
-            "mailto:user%02d at example.com" % (anotherNumber,),
-        ))
-        for attendee in attendees:
-            expected.remove(attendee.value())
-        self.assertEqual(len(expected), 0)
-
-
-    def test_everybodyInvitedAlready(self):
-        """
-        If the first so-many randomly selected users we come across
-        are already attendees on the event, the invitation attempt is
-        abandoned.
-        """
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
-        userNumber = 1
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-        inviter = RealisticInviter(Clock(), self.sim, client, userNumber)
-        inviter.setParameters(
-            inviteeDistribution=Deterministic(1),
-            inviteeCountDistribution=Deterministic(2)
-        )
-        inviter._invite()
-        self.assertEquals(len(client._events), 0)
-
-
-
-class AccepterTests(TestCase):
-    """
-    Tests for loadtest.profiles.Accepter.
-    """
-    def setUp(self):
-        self.sim = CalendarClientSimulator(
-            AnyUser(), Populator(None), None, None, None, None, None)
-
-
-    def test_enabled(self):
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-
-        accepter = Accepter(None, self.sim, client, userNumber, **{"enabled": False})
-        self.assertEqual(accepter.enabled, False)
-
-        accepter = Accepter(None, self.sim, client, userNumber, **{"enabled": True})
-        self.assertEqual(accepter.enabled, True)
-
-
-    def test_ignoreEventOnUnknownCalendar(self):
-        """
-        If an event on an unknown calendar changes, it is ignored.
-        """
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-        accepter = Accepter(None, self.sim, client, userNumber)
-        accepter.eventChanged('/some/calendar/1234.ics')
-
-
-    def test_ignoreNonCalendar(self):
-        """
-        If an event is on a calendar which is not of type
-        {CALDAV:}calendar, it is ignored.
-        """
-        userNumber = 14
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            csxml.dropbox_home, set(), u'notification', calendarURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[calendarURL] = calendar
-        accepter = Accepter(None, self.sim, client, userNumber)
-        accepter.eventChanged(calendarURL + '1234.ics')
-
-
-    def test_ignoreAccepted(self):
-        """
-        If the client is an attendee on an event but the PARTSTAT is
-        not NEEDS-ACTION, the event is ignored.
-        """
-        vevent = Component.fromString(ACCEPTED_EVENT)
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[calendarURL] = calendar
-        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
-        client._events[event.url] = event
-        accepter = Accepter(None, self.sim, client, userNumber)
-        accepter.eventChanged(event.url)
-
-
-    def test_ignoreAlreadyAccepting(self):
-        """
-        If the client sees an event change a second time before
-        responding to an invitation found on it during the first
-        change notification, the second change notification does not
-        generate another accept attempt.
-        """
-        clock = Clock()
-        randomDelay = 7
-        vevent = Component.fromString(INVITED_EVENT)
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[calendarURL] = calendar
-        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
-        client._events[event.url] = event
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.random = Deterministic()
-
-        def _gauss(mu, sigma):
-            return randomDelay
-        accepter.random.gauss = _gauss
-        accepter.eventChanged(event.url)
-        accepter.eventChanged(event.url)
-        clock.advance(randomDelay)
-
-
-    def test_inboxReply(self):
-        """
-        When an inbox item that contains a reply is seen by the client, it
-        deletes it immediately.
-        """
-        userNumber = 1
-        clock = Clock()
-        inboxURL = '/some/inbox/'
-        vevent = Component.fromString(INBOX_REPLY)
-        inbox = Calendar(
-            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[inboxURL] = inbox
-
-        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
-        client._setEvent(inboxEvent.url, inboxEvent)
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.eventChanged(inboxEvent.url)
-        clock.advance(3)
-        self.assertNotIn(inboxEvent.url, client._events)
-        self.assertNotIn('4321.ics', inbox.events)
-
-
-    def test_inboxReplyFailedDelete(self):
-        """
-        When an inbox item that contains a reply is seen by the client, it
-        deletes it immediately.  If the delete fails, the appropriate response
-        code is returned.
-        """
-        userNumber = 1
-        clock = Clock()
-        inboxURL = '/some/inbox/'
-        vevent = Component.fromString(INBOX_REPLY)
-        inbox = Calendar(
-            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[inboxURL] = inbox
-
-        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
-        client._setEvent(inboxEvent.url, inboxEvent)
-        client._failDeleteWithObject(inboxEvent.url, IncorrectResponseCode(
-            NO_CONTENT,
-            Response(
-                ('HTTP', 1, 1), PRECONDITION_FAILED,
-                'Precondition Failed', None, None))
-        )
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.eventChanged(inboxEvent.url)
-        clock.advance(3)
-        self.assertNotIn(inboxEvent.url, client._events)
-        self.assertNotIn('4321.ics', inbox.events)
-
-
-    def test_acceptInvitation(self):
-        """
-        If the client is an attendee on an event and the PARTSTAT is
-        NEEDS-ACTION, a response is generated which accepts the
-        invitation and the corresponding event in the
-        I{schedule-inbox} is deleted.
-        """
-        clock = Clock()
-        randomDelay = 7
-        vevent = Component.fromString(INVITED_EVENT)
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
-        client = StubClient(userNumber, self.mktemp())
-
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
-        client._calendars[calendarURL] = calendar
-
-        inboxURL = '/some/inbox/'
-        inbox = Calendar(
-            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
-        client._calendars[inboxURL] = inbox
-
-        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
-        client._setEvent(event.url, event)
-
-        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
-        client._setEvent(inboxEvent.url, inboxEvent)
-
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
-        accepter.eventChanged(event.url)
-        clock.advance(randomDelay)
-
-        vevent = client._events[event.url].component
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 2)
-        self.assertEquals(
-            attendees[1].parameterValue('CN'), 'User %02d' % (userNumber,))
-        self.assertEquals(
-            attendees[1].parameterValue('PARTSTAT'), 'ACCEPTED')
-        self.assertFalse(attendees[1].hasParameter('RSVP'))
-
-        self.assertNotIn(inboxEvent.url, client._events)
-        self.assertNotIn('4321.ics', inbox.events)
-
-
-    def test_reacceptInvitation(self):
-        """
-        If a client accepts an invitation on an event and then is
-        later re-invited to the same event, the invitation is again
-        accepted.
-        """
-        clock = Clock()
-        randomDelay = 7
-        vevent = Component.fromString(INVITED_EVENT)
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
-        client = StubClient(userNumber, self.mktemp())
-        client._calendars[calendarURL] = calendar
-        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
-        client._events[event.url] = event
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
-        accepter.eventChanged(event.url)
-        clock.advance(randomDelay)
-
-        # Now re-set the event so it has to be accepted again
-        event.component = Component.fromString(INVITED_EVENT)
-
-        # And now re-deliver it
-        accepter.eventChanged(event.url)
-        clock.advance(randomDelay)
-
-        # And ensure that it was accepted again
-        vevent = client._events[event.url].component
-        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
-        self.assertEquals(len(attendees), 2)
-        self.assertEquals(
-            attendees[1].parameterValue('CN'), 'User %02d' % (userNumber,))
-        self.assertEquals(
-            attendees[1].parameterValue('PARTSTAT'), 'ACCEPTED')
-        self.assertFalse(attendees[1].hasParameter('RSVP'))
-
-
-    def test_changeEventAttendeePreconditionFailed(self):
-        """
-        If the attempt to accept an invitation fails because of an
-        unmet precondition (412), the event is re-retrieved and the
-        PUT is re-issued with the new data.
-        """
-        clock = Clock()
-        userNumber = 2
-        client = StubClient(userNumber, self.mktemp())
-        randomDelay = 3
-
-        calendarURL = '/some/calendar/'
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
-        client._calendars[calendarURL] = calendar
-
-        vevent = Component.fromString(INVITED_EVENT)
-        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
-        client._setEvent(event.url, event)
-
-        accepter = Accepter(clock, self.sim, client, userNumber)
-        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
-
-        client.rescheduled.add(event.url)
-
-        accepter.eventChanged(event.url)
-        clock.advance(randomDelay)
-
-
-
-class EventerTests(TestCase):
-    """
-    Tests for loadtest.profiles.Eventer, a profile which adds new
-    events on calendars.
-    """
-    def setUp(self):
-        self.sim = CalendarClientSimulator(
-            AnyUser(), Populator(None), None, None, None, None, None)
-
-
-    def test_enabled(self):
-        userNumber = 13
-        client = StubClient(userNumber, self.mktemp())
-
-        eventer = Eventer(None, self.sim, client, None, **{"enabled": False})
-        self.assertEqual(eventer.enabled, False)
-
-        eventer = Eventer(None, self.sim, client, None, **{"enabled": True})
-        self.assertEqual(eventer.enabled, True)
-
-
-    def test_doNotAddEventOnInbox(self):
-        """
-        When the only calendar is a schedule inbox, no attempt is made
-        to add events on it.
-        """
-        calendar = Calendar(
-            caldavxml.schedule_inbox, set(), u'inbox', u'/sched/inbox', None)
-        client = StubClient(21, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-
-        eventer = Eventer(None, self.sim, client, None)
-        eventer._addEvent()
-
-        self.assertEquals(client._events, {})
-
-
-    def test_addEvent(self):
-        """
-        When there is a normal calendar to add events to,
-        L{Eventer._addEvent} adds an event to it.
-        """
-        calendar = Calendar(
-            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
-        client = StubClient(31, self.mktemp())
-        client._calendars.update({calendar.url: calendar})
-
-        eventer = Eventer(Clock(), self.sim, client, None)
-        eventer._addEvent()
-
-        self.assertEquals(len(client._events), 1)
-
-        # XXX Vary the event period/interval and the uid
-
-
-
-class OperationLoggerTests(TestCase):
-    """
-    Tests for L{OperationLogger}.
-    """
-    def test_noFailures(self):
-        """
-        If the median lag is below 1 second and the failure rate is below 1%,
-        L{OperationLogger.failures} returns an empty list.
-        """
-        logger = OperationLogger(outfile=StringIO())
-        logger.observe(dict(
-            type='operation', phase='start', user='user01',
-            label='testing', lag=0.5)
-        )
-        logger.observe(dict(
-            type='operation', phase='end', user='user01',
-            duration=0.35, label='testing', success=True)
-        )
-        self.assertEqual([], logger.failures())
-
-
-    def test_lagLimitExceeded(self):
-        """
-        If the median scheduling lag for any operation in the simulation
-        exceeds 1 second, L{OperationLogger.failures} returns a list containing
-        a string describing that issue.
-        """
-        logger = OperationLogger(outfile=StringIO())
-        for lag in [100.0, 1100.0, 1200.0]:
-            logger.observe(dict(
-                type='operation', phase='start', user='user01',
-                label='testing', lag=lag)
-            )
-        self.assertEqual(
-            ["Median TESTING scheduling lag greater than 1000.0ms"],
-            logger.failures())
-
-
-    def test_failureLimitExceeded(self):
-        """
-        If the failure rate for any operation exceeds 1%,
-        L{OperationLogger.failures} returns a list containing a string
-        describing that issue.
-        """
-        logger = OperationLogger(outfile=StringIO())
-        for _ignore in range(98):
-            logger.observe(dict(
-                type='operation', phase='end', user='user01',
-                duration=0.25, label='testing', success=True)
-            )
-        logger.observe(dict(
-            type='operation', phase='end', user='user01',
-            duration=0.25, label='testing', success=False)
-        )
-        self.assertEqual(
-            ["Greater than 1% TESTING failed"],
-            logger.failures())

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_sim.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_sim.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_sim.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,592 +0,0 @@
-##
-# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-from plistlib import writePlistToString
-from cStringIO import StringIO
-
-from twisted.python.log import msg
-from twisted.python.usage import UsageError
-from twisted.python.filepath import FilePath
-from twisted.internet.defer import Deferred, succeed
-from twisted.trial.unittest import TestCase
-
-from contrib.performance.stats import NormalDistribution
-from contrib.performance.loadtest.ical import OS_X_10_6
-from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
-from contrib.performance.loadtest.population import (
-    SmoothRampUp, ClientType, PopulationParameters, Populator, CalendarClientSimulator,
-    ProfileType, SimpleStatistics
-)
-from contrib.performance.loadtest.sim import (
-    Arrival, SimOptions, LoadSimulator, LagTrackingReactor,
-    _DirectoryRecord
-)
-
-
-VALID_CONFIG = {
-    'server': 'tcp:127.0.0.1:8008',
-    'webadmin': {
-        'enabled': True,
-        'HTTPPort': 8080,
-    },
-    'arrival': {
-        'factory': 'contrib.performance.loadtest.population.SmoothRampUp',
-        'params': {
-            'groups': 10,
-            'groupSize': 1,
-            'interval': 3,
-        },
-    },
-}
-
-VALID_CONFIG_PLIST = writePlistToString(VALID_CONFIG)
-
-
-
-class SimOptionsTests(TestCase):
-    def test_defaultConfig(self):
-        """
-        If the I{config} option is not specified, the default config.plist in
-        the source tree is used.
-        """
-        options = SimOptions()
-        self.assertEqual(options['config'], FilePath(__file__).sibling('config.plist'))
-
-
-    def test_configFileNotFound(self):
-        """
-        If the filename given to the I{config} option is not found,
-        L{SimOptions.parseOptions} raises a L{UsageError} indicating
-        this.
-        """
-        name = FilePath(self.mktemp())
-        options = SimOptions()
-        exc = self.assertRaises(
-            UsageError, options.parseOptions, ['--config', name.path])
-        self.assertEquals(
-            str(exc), "--config %s: No such file or directory" % (name.path,))
-
-
-    def test_configFileNotParseable(self):
-        """
-        If the contents of the file given to the I{config} option
-        cannot be parsed by L{ConfigParser},
-        L{SimOptions.parseOptions} raises a L{UsageError} indicating
-        this.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent("some random junk")
-        options = SimOptions()
-        exc = self.assertRaises(
-            UsageError, options.parseOptions, ['--config', config.path])
-        self.assertEquals(
-            str(exc),
-            "--config %s: syntax error: line 1, column 0" % (config.path,))
-
-
-
-class CalendarClientSimulatorTests(TestCase):
-    """
-    Tests for L{CalendarClientSimulator} which adds running clients to
-    a simulation.
-    """
-    realmName = 'stub'
-
-    def _user(self, name):
-        password = 'password-' + name
-        email = name + "@example.com"
-        record = _DirectoryRecord(name, password, name, email, name)
-        return record
-
-
-    def test_createUser(self):
-        """
-        Subsequent calls to L{CalendarClientSimulator._createUser}
-        with different user numbers return user details from different
-        directory records.
-        """
-        calsim = CalendarClientSimulator(
-            [self._user('alice'), self._user('bob'), self._user('carol')],
-            Populator(None), None, None, 'http://example.org:1234/', None, None)
-        users = sorted([
-            calsim._createUser(0)[0],
-            calsim._createUser(1)[0],
-            calsim._createUser(2)[0],
-        ])
-        self.assertEqual(['alice', 'bob', 'carol'], users)
-
-
-    def test_createUserAuthInfo(self):
-        """
-        The auth handler returned by L{CalendarClientSimulator._createUser}
-        includes the password taken from user's directory record.
-        """
-        calsim = CalendarClientSimulator(
-            [self._user('alice')],
-            Populator(None), None, None, 'http://example.org:1234/', None, None)
-        user, auth = calsim._createUser(0)
-        self.assertEqual(
-            auth['basic'].passwd.find_user_password('Test Realm', 'http://example.org:1234/')[1],
-            'password-' + user)
-        self.assertEqual(
-            auth['digest'].passwd.find_user_password('Test Realm', 'http://example.org:1234/')[1],
-            'password-' + user)
-
-
-    def test_stop(self):
-        """
-        After L{CalendarClientSimulator.stop} is called, failed clients and
-        profiles are not logged.
-        """
-        class BrokenClient(object):
-            def __init__(self, reactor, serverAddress, principalPathTemplate, serializationPath, userInfo, auth, runResult):
-                self._runResult = runResult
-
-            def run(self):
-                return self._runResult
-
-            def stop(self):
-                return succeed(None)
-
-        class BrokenProfile(object):
-            def __init__(self, reactor, simulator, client, userNumber, runResult):
-                self._runResult = runResult
-                self.enabled = True
-
-            def initialize(self):
-                return succeed(None)
-
-            def run(self):
-                return self._runResult
-
-        clientRunResult = Deferred()
-        profileRunResult = Deferred()
-
-        params = PopulationParameters()
-        params.addClient(1, ClientType(
-            BrokenClient, {'runResult': clientRunResult},
-            [ProfileType(BrokenProfile, {'runResult': profileRunResult})])
-        )
-        sim = CalendarClientSimulator(
-            [self._user('alice')], Populator(None), params, None, 'http://example.com:1234/', None, None)
-        sim.add(1, 1)
-        sim.stop()
-        clientRunResult.errback(RuntimeError("Some fictional client problem"))
-        profileRunResult.errback(RuntimeError("Some fictional profile problem"))
-
-        self.assertEqual([], self.flushLoggedErrors())
-
-
-
-class Reactor(object):
-    message = "some event to be observed"
-
-    def __init__(self):
-        self._triggers = []
-        self._whenRunning = []
-
-
-    def run(self):
-        for thunk in self._whenRunning:
-            thunk()
-        msg(thingo=self.message)
-        for _ignore_phase, event, thunk in self._triggers:
-            if event == 'shutdown':
-                thunk()
-
-
-    def callWhenRunning(self, thunk):
-        self._whenRunning.append(thunk)
-
-
-    def addSystemEventTrigger(self, phase, event, thunk):
-        self._triggers.append((phase, event, thunk))
-
-
-
-class Observer(object):
-    def __init__(self):
-        self.reported = False
-        self.events = []
-
-
-    def observe(self, event):
-        self.events.append(event)
-
-
-    def report(self, output):
-        self.reported = True
-
-
-    def failures(self):
-        return []
-
-
-
-class NullArrival(object):
-    def run(self, sim):
-        pass
-
-
-
-class StubSimulator(LoadSimulator):
-    def run(self):
-        return 3
-
-
-
-class LoadSimulatorTests(TestCase):
-    def test_main(self):
-        """
-        L{LoadSimulator.main} raises L{SystemExit} with the result of
-        L{LoadSimulator.run}.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(VALID_CONFIG_PLIST)
-
-        exc = self.assertRaises(
-            SystemExit, StubSimulator.main, ['--config', config.path])
-        self.assertEquals(
-            exc.args, (StubSimulator(None, None, None, None, None, None, None).run(),))
-
-
-    def test_createSimulator(self):
-        """
-        L{LoadSimulator.createSimulator} creates a L{CalendarClientSimulator}
-        with its own reactor and host and port information from the
-        configuration file.
-        """
-        server = 'http://127.0.0.7:1243/'
-        reactor = object()
-        sim = LoadSimulator(server, None, None, None, None, None, None, reactor=reactor)
-        calsim = sim.createSimulator()
-        self.assertIsInstance(calsim, CalendarClientSimulator)
-        self.assertIsInstance(calsim.reactor, LagTrackingReactor)
-        self.assertIdentical(calsim.reactor._reactor, reactor)
-        self.assertEquals(calsim.server, server)
-
-
-    def test_loadAccountsFromFile(self):
-        """
-        L{LoadSimulator.fromCommandLine} takes an account loader from the
-        config file and uses it to create user records for use in the
-        simulation.
-        """
-        accounts = FilePath(self.mktemp())
-        accounts.setContent("foo,bar,baz,quux,goo\nfoo2,bar2,baz2,quux2,goo2\n")
-        config = VALID_CONFIG.copy()
-        config["accounts"] = {
-            "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
-            "params": {
-                "path": accounts.path
-            },
-        }
-        configpath = FilePath(self.mktemp())
-        configpath.setContent(writePlistToString(config))
-        io = StringIO()
-        sim = LoadSimulator.fromCommandLine(['--config', configpath.path], io)
-        self.assertEquals(io.getvalue(), "Loaded 2 accounts.\n")
-        self.assertEqual(2, len(sim.records))
-        self.assertEqual(sim.records[0].uid, 'foo')
-        self.assertEqual(sim.records[0].password, 'bar')
-        self.assertEqual(sim.records[0].commonName, 'baz')
-        self.assertEqual(sim.records[0].email, 'quux')
-        self.assertEqual(sim.records[1].uid, 'foo2')
-        self.assertEqual(sim.records[1].password, 'bar2')
-        self.assertEqual(sim.records[1].commonName, 'baz2')
-        self.assertEqual(sim.records[1].email, 'quux2')
-
-
-    def test_loadDefaultAccountsFromFile(self):
-        """
-        L{LoadSimulator.fromCommandLine} takes an account loader (with
-        empty path)from the config file and uses it to create user
-        records for use in the simulation.
-        """
-        config = VALID_CONFIG.copy()
-        config["accounts"] = {
-            "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
-            "params": {
-                "path": ""
-            },
-        }
-        configpath = FilePath(self.mktemp())
-        configpath.setContent(writePlistToString(config))
-        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
-                                            StringIO())
-        self.assertEqual(99, len(sim.records))
-        self.assertEqual(sim.records[0].uid, 'user01')
-        self.assertEqual(sim.records[0].password, 'user01')
-        self.assertEqual(sim.records[0].commonName, 'User 01')
-        self.assertEqual(sim.records[0].email, 'user01 at example.com')
-        self.assertEqual(sim.records[98].uid, 'user99')
-        self.assertEqual(sim.records[98].password, 'user99')
-        self.assertEqual(sim.records[98].commonName, 'User 99')
-        self.assertEqual(sim.records[98].email, 'user99 at example.com')
-
-
-    def test_generateRecordsDefaultPatterns(self):
-        """
-        L{LoadSimulator.fromCommandLine} takes an account loader from the
-        config file and uses it to generate user records for use in the
-        simulation.
-        """
-        config = VALID_CONFIG.copy()
-        config["accounts"] = {
-            "loader": "contrib.performance.loadtest.sim.generateRecords",
-            "params": {
-                "count": 2
-            },
-        }
-        configpath = FilePath(self.mktemp())
-        configpath.setContent(writePlistToString(config))
-        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
-                                            StringIO())
-        self.assertEqual(2, len(sim.records))
-        self.assertEqual(sim.records[0].uid, 'user1')
-        self.assertEqual(sim.records[0].password, 'user1')
-        self.assertEqual(sim.records[0].commonName, 'User 1')
-        self.assertEqual(sim.records[0].email, 'user1 at example.com')
-        self.assertEqual(sim.records[1].uid, 'user2')
-        self.assertEqual(sim.records[1].password, 'user2')
-        self.assertEqual(sim.records[1].commonName, 'User 2')
-        self.assertEqual(sim.records[1].email, 'user2 at example.com')
-
-
-    def test_generateRecordsNonDefaultPatterns(self):
-        """
-        L{LoadSimulator.fromCommandLine} takes an account loader from the
-        config file and uses it to generate user records for use in the
-        simulation.
-        """
-        config = VALID_CONFIG.copy()
-        config["accounts"] = {
-            "loader": "contrib.performance.loadtest.sim.generateRecords",
-            "params": {
-                "count": 3,
-                "uidPattern": "USER%03d",
-                "passwordPattern": "PASSWORD%03d",
-                "namePattern": "Test User %03d",
-                "emailPattern": "USER%03d at example2.com",
-            },
-        }
-        configpath = FilePath(self.mktemp())
-        configpath.setContent(writePlistToString(config))
-        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
-                                            StringIO())
-        self.assertEqual(3, len(sim.records))
-        self.assertEqual(sim.records[0].uid, 'USER001')
-        self.assertEqual(sim.records[0].password, 'PASSWORD001')
-        self.assertEqual(sim.records[0].commonName, 'Test User 001')
-        self.assertEqual(sim.records[0].email, 'USER001 at example2.com')
-        self.assertEqual(sim.records[2].uid, 'USER003')
-        self.assertEqual(sim.records[2].password, 'PASSWORD003')
-        self.assertEqual(sim.records[2].commonName, 'Test User 003')
-        self.assertEqual(sim.records[2].email, 'USER003 at example2.com')
-
-
-    def test_specifyRuntime(self):
-        """
-        L{LoadSimulator.fromCommandLine} recognizes the I{--runtime} option to
-        specify a limit on how long the simulation will run.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(VALID_CONFIG_PLIST)
-        sim = LoadSimulator.fromCommandLine(['--config', config.path, '--runtime', '123'])
-        self.assertEqual(123, sim.runtime)
-
-
-    def test_loadServerConfig(self):
-        """
-        The Calendar Server host and port are loaded from the [server]
-        section of the configuration file specified.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(
-            writePlistToString({"server": "https://127.0.0.3:8432/"})
-        )
-        sim = LoadSimulator.fromCommandLine(['--config', config.path])
-        self.assertEquals(sim.server, "https://127.0.0.3:8432/")
-
-
-    def test_loadArrivalConfig(self):
-        """
-        The arrival policy type and arguments are loaded from the
-        [arrival] section of the configuration file specified.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(
-            writePlistToString({
-                "arrival": {
-                    "factory": "contrib.performance.loadtest.population.SmoothRampUp",
-                    "params": {
-                        "groups": 10,
-                        "groupSize": 1,
-                        "interval": 3,
-                    },
-                },
-            })
-        )
-        sim = LoadSimulator.fromCommandLine(['--config', config.path])
-        self.assertEquals(
-            sim.arrival,
-            Arrival(SmoothRampUp, dict(groups=10, groupSize=1, interval=3)))
-
-
-    def test_createArrivalPolicy(self):
-        """
-        L{LoadSimulator.createArrivalPolicy} creates an arrival
-        policy based on the L{Arrival} passed to its initializer.
-        """
-        class FakeArrival(object):
-            def __init__(self, reactor, x, y):
-                self.reactor = reactor
-                self.x = x
-                self.y = y
-
-        reactor = object()
-        sim = LoadSimulator(
-            None, None, None, None, None, Arrival(FakeArrival, {'x': 3, 'y': 2}), None, reactor=reactor)
-        arrival = sim.createArrivalPolicy()
-        self.assertIsInstance(arrival, FakeArrival)
-        self.assertIdentical(arrival.reactor, sim.reactor)
-        self.assertEquals(arrival.x, 3)
-        self.assertEquals(arrival.y, 2)
-
-
-    def test_loadPopulationParameters(self):
-        """
-        Client weights and profiles are loaded from the [clients]
-        section of the configuration file specified.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(
-            writePlistToString(
-                {
-                    "clients": [
-                        {
-                            "software": "contrib.performance.loadtest.ical.OS_X_10_6",
-                            "params": {
-                                "foo": "bar"
-                            },
-                            "profiles": [
-                                {
-                                    "params": {
-                                        "interval": 25,
-                                        "eventStartDistribution": {
-                                            "type": "contrib.performance.stats.NormalDistribution",
-                                            "params": {
-                                                "mu": 123,
-                                                "sigma": 456,
-                                            }
-                                        }
-                                    },
-                                    "class": "contrib.performance.loadtest.profiles.Eventer"
-                                }
-                            ],
-                            "weight": 3,
-                        }
-                    ]
-                }
-            )
-        )
-
-        sim = LoadSimulator.fromCommandLine(
-            ['--config', config.path, '--clients', config.path]
-        )
-        expectedParameters = PopulationParameters()
-        expectedParameters.addClient(
-            3,
-            ClientType(
-                OS_X_10_6,
-                {"foo": "bar"},
-                [
-                    ProfileType(
-                        Eventer, {
-                            "interval": 25,
-                            "eventStartDistribution": NormalDistribution(123, 456)
-                        }
-                    )
-                ]
-            )
-        )
-        self.assertEquals(sim.parameters, expectedParameters)
-
-
-    def test_requireClient(self):
-        """
-        At least one client is required, so if a configuration with an
-        empty clients array is specified, a single default client type
-        is used.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(writePlistToString({"clients": []}))
-        sim = LoadSimulator.fromCommandLine(
-            ['--config', config.path, '--clients', config.path]
-        )
-        expectedParameters = PopulationParameters()
-        expectedParameters.addClient(
-            1, ClientType(OS_X_10_6, {}, [Eventer, Inviter, Accepter]))
-        self.assertEquals(sim.parameters, expectedParameters)
-
-
-    def test_loadLogObservers(self):
-        """
-        Log observers specified in the [observers] section of the
-        configuration file are added to the logging system.
-        """
-        config = FilePath(self.mktemp())
-        config.setContent(
-            writePlistToString(
-                {
-                    "observers": [
-                        {
-                            "type": "contrib.performance.loadtest.population.SimpleStatistics",
-                            "params": {},
-                        },
-                    ]
-                }
-            )
-        )
-        sim = LoadSimulator.fromCommandLine(['--config', config.path])
-        self.assertEquals(len(sim.observers), 1)
-        self.assertIsInstance(sim.observers[0], SimpleStatistics)
-
-
-    def test_observeRunReport(self):
-        """
-        Each log observer is added to the log publisher before the
-        simulation run is started and has its C{report} method called
-        after the simulation run completes.
-        """
-        observers = [Observer()]
-        sim = LoadSimulator(
-            "http://example.com:123/",
-            "/principals/users/%s/",
-            None,
-            None,
-            None,
-            Arrival(lambda reactor: NullArrival(), {}),
-            None, observers, reactor=Reactor())
-        io = StringIO()
-        sim.run(io)
-        self.assertEquals(io.getvalue(), "\n*** PASS\n")
-        self.assertTrue(observers[0].reported)
-        self.assertEquals(
-            [e for e in observers[0].events if "thingo" in e][0]["thingo"],
-            Reactor.message
-        )

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_trafficlogger.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_trafficlogger.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_trafficlogger.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,210 +0,0 @@
-##
-# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-from zope.interface import Interface, implements
-
-from twisted.internet.protocol import ClientFactory, Protocol
-from twisted.trial.unittest import TestCase
-from twisted.test.proto_helpers import StringTransport, MemoryReactor
-from twisted.protocols.wire import Discard
-
-from contrib.performance.loadtest.trafficlogger import _TrafficLoggingFactory, loggedReactor
-
-
-class IProbe(Interface):
-    """
-    An interface which can be used to verify some interface-related behavior of
-    L{loggedReactor}.
-    """
-    def probe(): #@NoSelf
-        pass
-
-
-
-class Probe(object):
-    implements(IProbe)
-
-    _probed = False
-
-    def __init__(self, result=None):
-        self._result = result
-
-
-    def probe(self):
-        self._probed = True
-        return self._result
-
-
-
-class TrafficLoggingReactorTests(TestCase):
-    """
-    Tests for L{loggedReactor}.
-    """
-    def test_nothing(self):
-        """
-        L{loggedReactor} returns the object passed to it, if the object passed
-        to it doesn't provide any interfaces.  This is mostly for testing
-        convenience rather than a particularly useful feature.
-        """
-        probe = object()
-        self.assertIdentical(probe, loggedReactor(probe))
-
-
-    def test_interfaces(self):
-        """
-        The object returned by L{loggedReactor} provides all of the interfaces
-        provided by the object passed to it.
-        """
-        probe = Probe()
-        reactor = loggedReactor(probe)
-        self.assertTrue(IProbe.providedBy(reactor))
-
-
-    def test_passthrough(self):
-        """
-        Methods on interfaces on the object passed to L{loggedReactor} can be
-        called by calling them on the object returned by L{loggedReactor}.
-        """
-        expected = object()
-        probe = Probe(expected)
-        reactor = loggedReactor(probe)
-        result = reactor.probe()
-        self.assertTrue(probe._probed)
-        self.assertIdentical(expected, result)
-
-
-    def test_connectTCP(self):
-        """
-        Called on the object returned by L{loggedReactor}, C{connectTCP} calls
-        the wrapped reactor's C{connectTCP} method with the original factory
-        wrapped in a L{_TrafficLoggingFactory}.
-        """
-        class RecordDataProtocol(Protocol):
-            def dataReceived(self, data):
-                self.data = data
-        proto = RecordDataProtocol()
-        factory = ClientFactory()
-        factory.protocol = lambda: proto
-        reactor = MemoryReactor()
-        logged = loggedReactor(reactor)
-        logged.connectTCP('192.168.1.2', 1234, factory, 21, '127.0.0.2')
-        [(host, port, factory, timeout, bindAddress)] = reactor.tcpClients
-        self.assertEqual('192.168.1.2', host)
-        self.assertEqual(1234, port)
-        self.assertIsInstance(factory, _TrafficLoggingFactory)
-        self.assertEqual(21, timeout)
-        self.assertEqual('127.0.0.2', bindAddress)
-
-        # Verify that the factory and protocol specified are really being used
-        protocol = factory.buildProtocol(None)
-        protocol.makeConnection(None)
-        protocol.dataReceived("foo")
-        self.assertEqual(proto.data, "foo")
-
-
-    def test_getLogFiles(self):
-        """
-        The reactor returned by L{loggedReactor} has a C{getLogFiles} method
-        which returns a L{logstate} instance containing the active and
-        completed log files tracked by the logging wrapper.
-        """
-        wrapped = ClientFactory()
-        wrapped.protocol = Discard
-        reactor = MemoryReactor()
-        logged = loggedReactor(reactor)
-        logged.connectTCP('127.0.0.1', 1234, wrapped)
-        factory = reactor.tcpClients[0][2]
-
-        finished = factory.buildProtocol(None)
-        finished.makeConnection(StringTransport())
-        finished.dataReceived('finished')
-        finished.connectionLost(None)
-
-        active = factory.buildProtocol(None)
-        active.makeConnection(StringTransport())
-        active.dataReceived('active')
-
-        logs = logged.getLogFiles()
-        self.assertEqual(1, len(logs.finished))
-        self.assertIn('finished', logs.finished[0].getvalue())
-        self.assertEqual(1, len(logs.active))
-        self.assertIn('active', logs.active[0].getvalue())
-
-
-
-class TrafficLoggingFactoryTests(TestCase):
-    """
-    Tests for L{_TrafficLoggingFactory}.
-    """
-    def setUp(self):
-        self.wrapped = ClientFactory()
-        self.wrapped.protocol = Discard
-        self.factory = _TrafficLoggingFactory(self.wrapped)
-
-
-    def test_receivedBytesLogged(self):
-        """
-        When bytes are delivered through a protocol created by
-        L{_TrafficLoggingFactory}, they are added to a log kept on that
-        factory.
-        """
-        protocol = self.factory.buildProtocol(None)
-
-        # The factory should now have a new StringIO log file
-        self.assertEqual(1, len(self.factory.logs))
-
-        transport = StringTransport()
-        protocol.makeConnection(transport)
-
-        protocol.dataReceived("hello, world")
-        self.assertEqual(
-            "*\nC 0: 'hello, world'\n", self.factory.logs[0].getvalue())
-
-
-    def test_finishedLogs(self):
-        """
-        When connections are lost, the corresponding log files are moved into
-        C{_TrafficLoggingFactory.finishedLogs}.
-        """
-        protocol = self.factory.buildProtocol(None)
-        transport = StringTransport()
-        protocol.makeConnection(transport)
-        logfile = self.factory.logs[0]
-        protocol.connectionLost(None)
-        self.assertEqual(0, len(self.factory.logs))
-        self.assertEqual([logfile], self.factory.finishedLogs)
-
-
-    def test_finishedLogsLimit(self):
-        """
-        Only the most recent C{_TrafficLoggingFactory.LOGFILE_LIMIT} logfiles
-        are kept in C{_TrafficLoggingFactory.finishedLogs}.
-        """
-        self.factory.LOGFILE_LIMIT = 2
-        first = self.factory.buildProtocol(None)
-        first.makeConnection(StringTransport())
-        second = self.factory.buildProtocol(None)
-        second.makeConnection(StringTransport())
-        third = self.factory.buildProtocol(None)
-        third.makeConnection(StringTransport())
-
-        second.connectionLost(None)
-        first.connectionLost(None)
-        third.connectionLost(None)
-
-        self.assertEqual(
-            [first.logfile, third.logfile], self.factory.finishedLogs)

Deleted: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_webadmin.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_webadmin.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/test_webadmin.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -1,144 +0,0 @@
-##
-# Copyright (c) 2012-2015 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-##
-
-from twisted.trial.unittest import TestCase
-from contrib.performance.loadtest.webadmin import LoadSimAdminResource
-
-class WebAdminTests(TestCase):
-    """
-    Tests for L{LoadSimAdminResource}.
-    """
-
-    class FakeReporter(object):
-
-        def generateReport(self, output):
-            output.write("FakeReporter")
-
-
-    class FakeReactor(object):
-
-        def __init__(self):
-            self.running = True
-
-        def stop(self):
-            self.running = False
-
-
-    class FakeLoadSim(object):
-
-        def __init__(self):
-            self.reactor = WebAdminTests.FakeReactor()
-            self.reporter = WebAdminTests.FakeReporter()
-            self.running = True
-
-        def stop(self):
-            self.running = False
-
-
-    class FakeRequest(object):
-
-        def __init__(self, **kwargs):
-            self.args = kwargs
-
-
-    def test_resourceGET(self):
-        """
-        Test render_GET
-        """
-
-        loadsim = WebAdminTests.FakeLoadSim()
-        resource = LoadSimAdminResource(loadsim)
-
-        response = resource.render_GET(WebAdminTests.FakeRequest())
-        self.assertTrue(response.startswith("<html>"))
-        self.assertTrue(response.find(resource.token) != -1)
-
-
-    def test_resourcePOST_Stop(self):
-        """
-        Test render_POST when Stop button is clicked
-        """
-
-        loadsim = WebAdminTests.FakeLoadSim()
-        resource = LoadSimAdminResource(loadsim)
-        self.assertTrue(loadsim.reactor.running)
-
-        response = resource.render_POST(WebAdminTests.FakeRequest(
-            token=(resource.token,),
-            stop=None,
-        ))
-        self.assertTrue(response.startswith("<html>"))
-        self.assertTrue(response.find(resource.token) == -1)
-        self.assertTrue(response.find("FakeReporter") != -1)
-        self.assertFalse(loadsim.running)
-
-
-    def test_resourcePOST_Stop_BadToken(self):
-        """
-        Test render_POST when Stop button is clicked but token is wrong
-        """
-
-        loadsim = WebAdminTests.FakeLoadSim()
-        resource = LoadSimAdminResource(loadsim)
-        self.assertTrue(loadsim.reactor.running)
-
-        response = resource.render_POST(WebAdminTests.FakeRequest(
-            token=("xyz",),
-            stop=None,
-        ))
-        self.assertTrue(response.startswith("<html>"))
-        self.assertTrue(response.find(resource.token) != -1)
-        self.assertTrue(response.find("FakeReporter") == -1)
-        self.assertTrue(loadsim.running)
-
-
-    def test_resourcePOST_Results(self):
-        """
-        Test render_POST when Results button is clicked
-        """
-
-        loadsim = WebAdminTests.FakeLoadSim()
-        resource = LoadSimAdminResource(loadsim)
-        self.assertTrue(loadsim.reactor.running)
-
-        response = resource.render_POST(WebAdminTests.FakeRequest(
-            token=(resource.token,),
-            results=None,
-        ))
-        self.assertTrue(response.startswith("<html>"))
-        self.assertTrue(response.find(resource.token) != -1)
-        self.assertTrue(response.find("FakeReporter") != -1)
-        self.assertTrue(loadsim.running)
-
-
-    def test_resourcePOST_Results_BadToken(self):
-        """
-        Test render_POST when Results button is clicked and token is wrong
-        """
-
-        loadsim = WebAdminTests.FakeLoadSim()
-        resource = LoadSimAdminResource(loadsim)
-        self.assertTrue(loadsim.reactor.running)
-
-        response = resource.render_POST(WebAdminTests.FakeRequest(
-            token=("xyz",),
-            results=None,
-        ))
-        self.assertTrue(response.startswith("<html>"))
-        self.assertTrue(response.find(resource.token) != -1)
-        self.assertTrue(response.find("FakeReporter") == -1)
-        self.assertTrue(loadsim.running)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/__init__.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/__init__.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,27 @@
+##
+# Copyright (c) 2012-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Unit tests for load-testing tool
+Tests:
+  ical.py
+  logger.py
+  population.py
+  profiles.py
+  sim.py
+  trafficlogger.py
+  webadmin.py
+"""

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_distributions.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_distributions.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_distributions.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,167 @@
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.distributions import (
+    # Continuous distributions
+    LogNormalDistribution, NormalDistribution,
+    # Discrete distributions
+    UniformDiscreteDistribution, UniformIntegerDistribution,
+    BernoulliDistribution, BinomialDistribution, FixedDistribution,
+    # Calendar-specific distributions
+    WorkDistribution, RecurrenceDistribution,
+)
+
+from pycalendar.datetime import DateTime
+from pycalendar.timezone import Timezone
+
+from scipy import stats
+from scipy.optimize import curve_fit
+import itertools
+
+"""
+Disclaimer: These tests are nondeterministic, so be careful
+"""
+
+class DistributionTestBase(TestCase):
+    def get_n_samples(self, dist, n):
+        samples = []
+        for _ignore_i in xrange(n):
+            samples.append(dist.sample())
+        return samples
+
+class DiscreteDistributionTests(DistributionTestBase):
+    def test_bernoulli(self):
+        sample_count = 1000
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for prop in proportions:
+            dist = BernoulliDistribution(proportion=prop)
+            samples = self.get_n_samples(dist, sample_count)
+            successes = samples.count(True)
+
+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value <= 0.01, "%d/%d, expected %f" % (successes, sample_count, prop))
+    test_bernoulli.skip = "FIXME: Sometimes doesn't pass...  551/1000, expected 0.500000"
+
+    def test_binomial(self):
+        sample_counts = [100, 1000, 10000]
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for sample_count, prop in itertools.product(sample_counts, proportions):
+            dist = BinomialDistribution(p=prop, n=sample_count)
+            successes = dist.sample()
+
+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value <= 0.01, "%d/%d, expected %f" % (successes, sample_count, prop))
+
+    def test_fixed(self):
+        dist = FixedDistribution(4) # https://xkcd.com/221/
+        for _ignore_i in xrange(100):
+            self.assertEqual(dist.sample(), 4)
+
+    def test_uniformdiscrete(self):
+        population = [82, 101, 100, 109, 111, 110, 100]
+        counts = dict.fromkeys(population, 0)
+        dist = UniformDiscreteDistribution(population)
+        for _ignore_i in range(len(population) * 10):
+            counts[dist.sample()] += 1
+        self.assertEqual(dict.fromkeys(population, 10), counts)
+        # Do some chi squared stuff
+    test_uniformdiscrete.skip = "FIXME: Investigate and fix"
+
+    def test_uniform(self):
+        dist = UniformIntegerDistribution(-5, 10)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertTrue(-5 <= value < 10)
+            self.assertIsInstance(value, int)
+
+class ContinuousDistributionTests(TestCase):
+    def is_fit(self, pdf, xdata, ydata, pexp):
+        """
+        expected parameters
+        """
+        popt, pcov = curve_fit(pdf, xdata, ydata)
+        print popt
+
+    def test_normal(self):
+        dist = NormalDistribution()
+        dist
+    test_normal.skip = "FIXME: Finish writing this test"
+
+    def test_lognormal(self):
+        dist = LogNormalDistribution(mu=1, sigma=1)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertIsInstance(value, float)
+            self.assertTrue(value >= 0.0, "negative value %r" % (value,))
+            self.assertTrue(value <= 1000, "implausibly high value %r" % (value,))
+
+        dist = LogNormalDistribution(mode=1, median=2)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertIsInstance(value, float)
+            self.assertTrue(value >= 0.0, "negative value %r" % (value,))
+            self.assertTrue(value <= 1000, "implausibly high value %r" % (value,))
+
+        dist = LogNormalDistribution(mode=1, mean=2)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertIsInstance(value, float)
+            self.assertTrue(value >= 0.0, "negative value %r" % (value,))
+            self.assertTrue(value <= 1000, "implausibly high value %r" % (value,))
+
+        self.assertRaises(ValueError, LogNormalDistribution, mu=1)
+        self.assertRaises(ValueError, LogNormalDistribution, sigma=1)
+        self.assertRaises(ValueError, LogNormalDistribution, mode=1)
+        self.assertRaises(ValueError, LogNormalDistribution, mean=1)
+        self.assertRaises(ValueError, LogNormalDistribution, median=1)
+
+class CalendarDistributionTests(TestCase):
+
+    def test_workdistribution(self):
+        tzname = "US/Eastern"
+        dist = WorkDistribution(["mon", "wed", "thu", "sat"], 10, 20, tzname)
+        dist._helperDistribution = UniformDiscreteDistribution([35 * 60 * 60 + 30 * 60])
+        dist.now = lambda tzname = None: DateTime(2011, 5, 29, 18, 5, 36, tzid=tzname)
+        value = dist.sample()
+        self.assertEqual(
+            # Move past three workdays - monday, wednesday, thursday - using 30
+            # of the hours, and then five and a half hours into the fourth
+            # workday, saturday.  Workday starts at 10am, so the sample value
+            # is 3:30pm, ie 1530 hours.
+            DateTime(2011, 6, 4, 15, 30, 0, tzid=Timezone(tzid=tzname)),
+            value
+        )
+
+        dist = WorkDistribution(["mon", "tue", "wed", "thu", "fri"], 10, 20, tzname)
+        dist._helperDistribution = UniformDiscreteDistribution([35 * 60 * 60 + 30 * 60])
+        value = dist.sample()
+        self.assertTrue(isinstance(value, DateTime))
+
+    # twisted.trial.unittest.FailTest: not equal:
+    # a = datetime.datetime(2011, 6, 4, 15, 30, tzinfo=<DstTzInfo 'US/Eastern' EST-1 day, 19:00:00 STD>)
+    # b = datetime.datetime(2011, 6, 4, 19, 30, tzinfo=<DstTzInfo 'US/Eastern' EDT-1 day, 20:00:00 DST>)
+    # test_workdistribution.todo = "Somehow timezones mess this up"
+
+
+    def test_recurrencedistribution(self):
+        dist = RecurrenceDistribution(False)
+        for _ignore in range(100):
+            value = dist.sample()
+            self.assertTrue(value is None)
+
+        dist = RecurrenceDistribution(True, {"daily": 1, "none": 2, "weekly": 1})
+        dist._helperDistribution = UniformDiscreteDistribution([0, 3, 2, 1, 0])
+        value = dist.sample()
+        self.assertTrue(value is not None)
+        value = dist.sample()
+        self.assertTrue(value is None)
+        value = dist.sample()
+        self.assertTrue(value is None)
+        value = dist.sample()
+        self.assertTrue(value is not None)
+        value = dist.sample()
+        self.assertTrue(value is not None)
+    test_recurrencedistribution.skip = "FIXME: Investigate and fix"

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_ical.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_ical.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_ical.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,1974 @@
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+from caldavclientlibrary.protocol.caldav.definitions import caldavxml
+from caldavclientlibrary.protocol.caldav.definitions import csxml
+from caldavclientlibrary.protocol.url import URL
+from caldavclientlibrary.protocol.webdav.definitions import davxml
+
+from contrib.performance.httpclient import MemoryConsumer, StringProducer
+from contrib.performance.loadtest.ical import Event, Calendar
+from contrib.performance.loadtest.records import DirectoryRecord
+from contrib.performance.loadtest.clients import OS_X_10_6
+
+from pycalendar.datetime import DateTime
+from pycalendar.timezone import Timezone
+
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.protocol import ProtocolToConsumerAdapter
+from twisted.python.failure import Failure
+from twisted.trial.unittest import TestCase
+from twisted.web.client import ResponseDone
+from twisted.web.http import OK, NO_CONTENT, CREATED, MULTI_STATUS
+from twisted.web.http_headers import Headers
+
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE # FIXME: to get TimezoneCache to work
+from twistedcaldav.ical import Component
+from twistedcaldav.timezones import TimezoneCache
+
+import json
+import os
+
+EVENT_UID = 'D94F247D-7433-43AF-B84B-ADD684D023B0'
+
+EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20101018T155454Z
+UID:%(UID)s
+DTEND;TZID=America/New_York:20101028T130000
+ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
+ TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
+ m
+ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:user01@
+ example.com
+TRANSP:OPAQUE
+SUMMARY:Attended Event
+DTSTART;TZID=America/New_York:20101028T120000
+DTSTAMP:20101018T155513Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {'UID': EVENT_UID}
+
+EVENT_INVITE = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+X-LIC-LOCATION:America/New_York
+BEGIN:STANDARD
+DTSTART:18831118T120358
+RDATE:18831118T120358
+TZNAME:EST
+TZOFFSETFROM:-045602
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19180331T020000
+RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19181027T020000
+RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19200101T000000
+RDATE:19200101T000000
+RDATE:19420101T000000
+RDATE:19460101T000000
+RDATE:19670101T000000
+TZNAME:EST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19200328T020000
+RDATE:19200328T020000
+RDATE:19740106T020000
+RDATE:19750223T020000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19201031T020000
+RDATE:19201031T020000
+RDATE:19450930T020000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19210424T020000
+RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19210925T020000
+RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19420209T020000
+RDATE:19420209T020000
+TZNAME:EWT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19450814T190000
+RDATE:19450814T190000
+TZNAME:EPT
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19460428T020000
+RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19460929T020000
+RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19551030T020000
+RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19760425T020000
+RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20101018T155454Z
+UID:%(UID)s
+DTEND;TZID=America/New_York:20101028T130000
+ATTENDEE;CN="User 02";CUTYPE=INDIVIDUAL;EMAIL="user02 at example.com";PARTS
+ TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user02 at example.co
+ m
+ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
+ TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
+ m
+ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:user01
+TRANSP:OPAQUE
+SUMMARY:Attended Event
+DTSTART;TZID=America/New_York:20101028T120000
+DTSTAMP:20101018T155513Z
+ORGANIZER;CN="User 01":urn:uuid:user01
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {'UID': EVENT_UID}
+
+EVENT_AND_TIMEZONE = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+X-LIC-LOCATION:America/New_York
+BEGIN:STANDARD
+DTSTART:18831118T120358
+RDATE:18831118T120358
+TZNAME:EST
+TZOFFSETFROM:-045602
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19180331T020000
+RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19181027T020000
+RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19200101T000000
+RDATE:19200101T000000
+RDATE:19420101T000000
+RDATE:19460101T000000
+RDATE:19670101T000000
+TZNAME:EST
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19200328T020000
+RDATE:19200328T020000
+RDATE:19740106T020000
+RDATE:19750223T020000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19201031T020000
+RDATE:19201031T020000
+RDATE:19450930T020000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19210424T020000
+RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19210925T020000
+RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19420209T020000
+RDATE:19420209T020000
+TZNAME:EWT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19450814T190000
+RDATE:19450814T190000
+TZNAME:EPT
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19460428T020000
+RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19460929T020000
+RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:19551030T020000
+RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19760425T020000
+RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20101018T155454Z
+UID:%(UID)s
+DTEND;TZID=America/New_York:20101028T130000
+ATTENDEE;CN="User 03";CUTYPE=INDIVIDUAL;EMAIL="user03 at example.com";PARTS
+ TAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:user03 at example.co
+ m
+ATTENDEE;CN="User 01";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:user01@
+ example.com
+TRANSP:OPAQUE
+SUMMARY:Attended Event
+DTSTART;TZID=America/New_York:20101028T120000
+DTSTAMP:20101018T155513Z
+ORGANIZER;CN="User 01":mailto:user01 at example.com
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % {'UID': EVENT_UID}
+
+
+PRINCIPAL_PROPFIND_RESPONSE = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/principals/__uids__/user01/</href>
+    <propstat>
+      <prop>
+        <principal-collection-set>
+          <href>/principals/</href>
+        </principal-collection-set>
+        <calendar-home-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01</href>
+        </calendar-home-set>
+        <calendar-user-address-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/principals/__uids__/user01/</href>
+          <href xmlns='DAV:'>/principals/users/user01/</href>
+        </calendar-user-address-set>
+        <schedule-inbox-URL xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/inbox/</href>
+        </schedule-inbox-URL>
+        <schedule-outbox-URL xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/outbox/</href>
+        </schedule-outbox-URL>
+        <dropbox-home-URL xmlns='http://calendarserver.org/ns/'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/dropbox/</href>
+        </dropbox-home-URL>
+        <notification-URL xmlns='http://calendarserver.org/ns/'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/notification/</href>
+        </notification-URL>
+        <displayname>User 01</displayname>
+        <principal-URL>
+          <href>/principals/__uids__/user01/</href>
+        </principal-URL>
+        <supported-report-set>
+          <supported-report>
+            <report>
+              <acl-principal-prop-set/>
+            </report>
+          </supported-report>
+          <supported-report>
+            <report>
+              <principal-match/>
+            </report>
+          </supported-report>
+          <supported-report>
+            <report>
+              <principal-property-search/>
+            </report>
+          </supported-report>
+          <supported-report>
+            <report>
+              <expand-property/>
+            </report>
+          </supported-report>
+        </supported-report-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+</multistatus>
+"""
+
+_CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/calendars/__uids__/user01/</href>
+    <propstat>
+      <prop>
+        %(xmpp)s
+        <displayname>User 01</displayname>
+        <resourcetype>
+          <collection/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+        </current-user-privilege-set>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'>/Some/Unique/Value</pushkey>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <getctag xmlns='http://calendarserver.org/ns/'/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/notification/</href>
+    <propstat>
+      <prop>
+        <displayname>notification</displayname>
+        <resourcetype>
+          <collection/>
+          <notification xmlns='http://calendarserver.org/ns/'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+        </current-user-privilege-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <getctag xmlns='http://calendarserver.org/ns/'/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/dropbox/</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <collection/>
+          <dropbox-home xmlns='http://calendarserver.org/ns/'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+        </current-user-privilege-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <getctag xmlns='http://calendarserver.org/ns/'/>
+        <displayname/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/calendar/</href>
+    <propstat>
+      <prop>
+        <getctag xmlns='http://calendarserver.org/ns/'>c2696540-4c4c-4a31-adaf-c99630776828#3</getctag>
+        <displayname>calendar</displayname>
+        <calendar-color xmlns='http://apple.com/ns/ical/'>#0252D4FF</calendar-color>
+        <calendar-order xmlns='http://apple.com/ns/ical/'>1</calendar-order>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <comp name='VEVENT'/>
+          <comp name='VTODO'/>
+          <comp name='VTIMEZONE'/>
+          <comp name='VFREEBUSY'/>
+        </supported-calendar-component-set>
+        <resourcetype>
+          <collection/>
+          <calendar xmlns='urn:ietf:params:xml:ns:caldav'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'>
+          <opaque/>
+        </schedule-calendar-transp>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0500
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:EDT
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:EST
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+]]></calendar-timezone>
+        <current-user-privilege-set>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+        </current-user-privilege-set>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/outbox/</href>
+    <propstat>
+      <prop>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <comp name='VEVENT'/>
+          <comp name='VTODO'/>
+          <comp name='VTIMEZONE'/>
+          <comp name='VFREEBUSY'/>
+        </supported-calendar-component-set>
+        <resourcetype>
+          <collection/>
+          <schedule-outbox xmlns='urn:ietf:params:xml:ns:caldav'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+          <privilege>
+            <schedule-send xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+        </current-user-privilege-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <getctag xmlns='http://calendarserver.org/ns/'/>
+        <displayname/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/freebusy</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <free-busy-url xmlns='http://calendarserver.org/ns/'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <schedule-deliver xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+        </current-user-privilege-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <getctag xmlns='http://calendarserver.org/ns/'/>
+        <displayname/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/calendars/__uids__/user01/inbox/</href>
+    <propstat>
+      <prop>
+        <getctag xmlns='http://calendarserver.org/ns/'>a483dab3-1391-445b-b1c3-5ae9dfc81c2f#0</getctag>
+        <displayname>inbox</displayname>
+        <supported-calendar-component-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <comp name='VEVENT'/>
+          <comp name='VTODO'/>
+          <comp name='VTIMEZONE'/>
+          <comp name='VFREEBUSY'/>
+        </supported-calendar-component-set>
+        <resourcetype>
+          <collection/>
+          <schedule-inbox xmlns='urn:ietf:params:xml:ns:caldav'/>
+        </resourcetype>
+        <owner>
+          <href>/principals/__uids__/user01/</href>
+        </owner>
+        <calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/calendar</href>
+        </calendar-free-busy-set>
+        <schedule-default-calendar-URL xmlns='urn:ietf:params:xml:ns:caldav'>
+          <href xmlns='DAV:'>/calendars/__uids__/user01/calendar</href>
+        </schedule-default-calendar-URL>
+        <quota-available-bytes>104855434</quota-available-bytes>
+        <quota-used-bytes>2166</quota-used-bytes>
+        <current-user-privilege-set>
+          <privilege>
+            <schedule-deliver xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <schedule xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+          <privilege>
+            <all/>
+          </privilege>
+          <privilege>
+            <read/>
+          </privilege>
+          <privilege>
+            <write/>
+          </privilege>
+          <privilege>
+            <write-properties/>
+          </privilege>
+          <privilege>
+            <write-content/>
+          </privilege>
+          <privilege>
+            <bind/>
+          </privilege>
+          <privilege>
+            <unbind/>
+          </privilege>
+          <privilege>
+            <unlock/>
+          </privilege>
+          <privilege>
+            <read-acl/>
+          </privilege>
+          <privilege>
+            <write-acl/>
+          </privilege>
+          <privilege>
+            <read-current-user-privilege-set/>
+          </privilege>
+          <privilege>
+            <read-free-busy xmlns='urn:ietf:params:xml:ns:caldav'/>
+          </privilege>
+        </current-user-privilege-set>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>
+        <calendar-description xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-color xmlns='http://apple.com/ns/ical/'/>
+        <calendar-order xmlns='http://apple.com/ns/ical/'/>
+        <schedule-calendar-transp xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <calendar-timezone xmlns='urn:ietf:params:xml:ns:caldav'/>
+        <source xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-alarms xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-attachments xmlns='http://calendarserver.org/ns/'/>
+        <subscribed-strip-todos xmlns='http://calendarserver.org/ns/'/>
+        <refreshrate xmlns='http://apple.com/ns/ical/'/>
+        <push-transports xmlns='http://calendarserver.org/ns/'/>
+        <pushkey xmlns='http://calendarserver.org/ns/'/>
+        <publish-url xmlns='http://calendarserver.org/ns/'/>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+</multistatus>
+"""
+
+CALENDAR_HOME_PROPFIND_RESPONSE = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {
+    "xmpp": """\
+        <xmpp-server xmlns='http://calendarserver.org/ns/'/>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'/>""",
+}
+
+CALENDAR_HOME_PROPFIND_RESPONSE_WITH_XMPP = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {
+    "xmpp": """\
+        <xmpp-server xmlns='http://calendarserver.org/ns/'>xmpp.example.invalid:1952</xmpp-server>
+        <xmpp-uri xmlns='http://calendarserver.org/ns/'>xmpp:pubsub.xmpp.example.invalid?pubsub;node=/CalDAV/another.example.invalid/user01/</xmpp-uri>""",
+}
+
+CALENDAR_HOME_PROPFIND_RESPONSE_XMPP_MISSING = _CALENDAR_HOME_PROPFIND_RESPONSE_TEMPLATE % {"xmpp": ""}
+
+
+
+class MemoryResponse(object):
+    def __init__(self, version, code, phrase, headers, bodyProducer):
+        self.version = version
+        self.code = code
+        self.phrase = phrase
+        self.headers = headers
+        self.length = bodyProducer.length
+        self._bodyProducer = bodyProducer
+
+
+    def deliverBody(self, protocol):
+        protocol.makeConnection(self._bodyProducer)
+        d = self._bodyProducer.startProducing(ProtocolToConsumerAdapter(protocol))
+        d.addCallback(lambda ignored: protocol.connectionLost(Failure(ResponseDone())))
+
+
+
+class OS_X_10_6Mixin:
+    """
+    Mixin for L{TestCase}s for L{OS_X_10_6}.
+    """
+    def setUp(self):
+        TimezoneCache.create()
+        self.record = DirectoryRecord(
+            u"user91", u"user91", u"User 91", u"user91 at example.org", u"user91",
+        )
+        serializePath = self.mktemp()
+        os.mkdir(serializePath)
+        self.client = OS_X_10_6(
+            None,
+            "http://127.0.0.1",
+            # "/principals/users/%s/",
+            serializePath,
+            self.record,
+            None,
+        )
+
+
+    def interceptRequests(self):
+        requests = []
+        def request(*args, **kwargs):
+            result = Deferred()
+            requests.append((result, args))
+            return result
+        self.client._request = request
+        return requests
+
+
+
+class OS_X_10_6Tests(OS_X_10_6Mixin, TestCase):
+    """
+    Tests for L{OS_X_10_6}.
+    """
+    def test_parsePrincipalPROPFINDResponse(self):
+        """
+        L{Principal._parsePROPFINDResponse} accepts an XML document
+        like the one in the response to a I{PROPFIND} request for
+        I{/principals/__uids__/<uid>/} and returns a C{PropFindResult}
+        representing the data from it.
+        """
+        principals = self.client._parseMultiStatus(PRINCIPAL_PROPFIND_RESPONSE)
+        principal = principals['/principals/__uids__/user01/']
+        self.assertEquals(
+            principal.getHrefProperties(),
+            {
+                davxml.principal_collection_set: URL(path='/principals/'),
+                caldavxml.calendar_home_set: URL(path='/calendars/__uids__/user01'),
+                caldavxml.calendar_user_address_set: (
+                    URL(path='/principals/__uids__/user01/'),
+                    URL(path='/principals/users/user01/'),
+                ),
+                caldavxml.schedule_inbox_URL: URL(path='/calendars/__uids__/user01/inbox/'),
+                caldavxml.schedule_outbox_URL: URL(path='/calendars/__uids__/user01/outbox/'),
+                csxml.dropbox_home_URL: URL(path='/calendars/__uids__/user01/dropbox/'),
+                csxml.notification_URL: URL(path='/calendars/__uids__/user01/notification/'),
+                davxml.principal_URL: URL(path='/principals/__uids__/user01/'),
+            }
+        )
+        self.assertEquals(
+            principal.getTextProperties(),
+            {davxml.displayname: 'User 01'})
+
+#         self.assertEquals(
+#             principal.getSomething(),
+#             {SUPPORTED_REPORT_SET: (
+#                     '{DAV:}acl-principal-prop-set',
+#                     '{DAV:}principal-match',
+#                     '{DAV:}principal-property-search',
+#                     '{DAV:}expand-property',
+#                     )})
+
+
+    def test_extractCalendars(self):
+        """
+        L{OS_X_10_6._extractCalendars} accepts a calendar home
+        PROPFIND response body and returns a list of calendar objects
+        constructed from the data extracted from the response.
+        """
+        home = "/calendars/__uids__/user01/"
+        calendars = self.client._extractCalendars(
+            self.client._parseMultiStatus(CALENDAR_HOME_PROPFIND_RESPONSE), home)
+        calendars.sort(key=lambda cal: cal.resourceType)
+        calendar, inbox = calendars
+
+        self.assertEquals(calendar.resourceType, caldavxml.calendar)
+        self.assertEquals(calendar.name, "calendar")
+        self.assertEquals(calendar.url, "/calendars/__uids__/user01/calendar/")
+        self.assertEquals(calendar.changeToken, "c2696540-4c4c-4a31-adaf-c99630776828#3")
+
+        self.assertEquals(inbox.resourceType, caldavxml.schedule_inbox)
+        self.assertEquals(inbox.name, "inbox")
+        self.assertEquals(inbox.url, "/calendars/__uids__/user01/inbox/")
+        self.assertEquals(inbox.changeToken, "a483dab3-1391-445b-b1c3-5ae9dfc81c2f#0")
+
+        self.assertEqual({}, self.client.xmpp)
+
+
+
+    @inlineCallbacks
+    def test_changeEventAttendee(self):
+        """
+        OS_X_10_6.changeEventAttendee removes one attendee from an
+        existing event and appends another.
+        """
+        requests = self.interceptRequests()
+
+        vevent = Component.fromString(EVENT)
+        attendees = tuple(vevent.mainComponent().properties("ATTENDEE"))
+        old = attendees[0]
+        new = old.duplicate()
+        new.setParameter('CN', 'Some Other Guy')
+        event = Event(self.client.serializeLocation(), u'/some/calendar/1234.ics', None, vevent)
+        self.client._events[event.url] = event
+        self.client.changeEventAttendee(event.url, old, new)
+
+        _ignore_result, req = requests.pop(0)
+
+        # iCal PUTs the new VCALENDAR object.
+        _ignore_expectedResponseCode, method, url, headers, body = req
+        self.assertEquals(method, 'PUT')
+        self.assertEquals(url, 'http://127.0.0.1' + event.url)
+        self.assertIsInstance(url, str)
+        self.assertEquals(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+        consumer = MemoryConsumer()
+        yield body.startProducing(consumer)
+        vevent = Component.fromString(consumer.value())
+        attendees = tuple(vevent.mainComponent().properties("ATTENDEE"))
+        self.assertEquals(len(attendees), 2)
+        self.assertEquals(attendees[0].parameterValue('CN'), 'User 01')
+        self.assertEquals(attendees[1].parameterValue('CN'), 'Some Other Guy')
+
+
+    def test_addEvent(self):
+        """
+        L{OS_X_10_6.addEvent} PUTs the event passed to it to the
+        server and updates local state to reflect its existence.
+        """
+        requests = self.interceptRequests()
+
+        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/mumble/', None)
+        self.client._calendars[calendar.url] = calendar
+
+        vcalendar = Component.fromString(EVENT)
+        d = self.client.addEvent(u'/mumble/frotz.ics', vcalendar)
+
+        result, req = requests.pop(0)
+
+        # iCal PUTs the new VCALENDAR object.
+        expectedResponseCode, method, url, headers, body = req
+        self.assertEqual(expectedResponseCode, CREATED)
+        self.assertEqual(method, 'PUT')
+        self.assertEqual(url, 'http://127.0.0.1/mumble/frotz.ics')
+        self.assertIsInstance(url, str)
+        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+        consumer = MemoryConsumer()
+        finished = body.startProducing(consumer)
+        def cbFinished(ignored):
+            self.assertEqual(
+                Component.fromString(consumer.value()),
+                Component.fromString(EVENT_AND_TIMEZONE))
+        finished.addCallback(cbFinished)
+
+        def requested(ignored):
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), CREATED, "Created", Headers({}),
+                StringProducer(""))
+            result.callback(response)
+        finished.addCallback(requested)
+
+        return d
+
+
+    @inlineCallbacks
+    def test_addInvite(self):
+        """
+        L{OS_X_10_6.addInvite} PUTs the event passed to it to the
+        server and updates local state to reflect its existence, but
+        it also does attendee auto-complete and free-busy checks before
+        the PUT.
+        """
+
+        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/mumble/', None)
+        self.client._calendars[calendar.url] = calendar
+
+        vcalendar = Component.fromString(EVENT_INVITE)
+
+        self.client.uuid = u'urn:uuid:user01'
+        self.client.email = u'mailto:user01 at example.com'
+        self.client.principalCollection = "/principals/"
+        self.client.outbox = "/calendars/__uids__/user01/outbox/"
+
+        @inlineCallbacks
+        def _testReport(*args, **kwargs):
+            expectedResponseCode, method, url, headers, body = args
+            self.assertEqual(expectedResponseCode, (MULTI_STATUS,))
+            self.assertEqual(method, 'REPORT')
+            self.assertEqual(url, 'http://127.0.0.1/principals/')
+            self.assertIsInstance(url, str)
+            self.assertEqual(headers.getRawHeaders('content-type'), ['text/xml'])
+
+            consumer = MemoryConsumer()
+            yield body.startProducing(consumer)
+
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "MultiStatus", Headers({}),
+                StringProducer("<?xml version='1.0' encoding='UTF-8'?><multistatus xmlns='DAV:' />"))
+
+            returnValue(response)
+
+        @inlineCallbacks
+        def _testPost(*args, **kwargs):
+            expectedResponseCode, method, url, headers, body = args
+            self.assertEqual(expectedResponseCode, OK)
+            self.assertEqual(method, 'POST')
+            self.assertEqual(url, 'http://127.0.0.1/calendars/__uids__/user01/outbox/')
+            self.assertIsInstance(url, str)
+            self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+            consumer = MemoryConsumer()
+            yield body.startProducing(consumer)
+            self.assertNotEqual(consumer.value().find(kwargs["attendee"]), -1)
+
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), OK, "OK", Headers({}),
+                StringProducer(""))
+
+            returnValue(response)
+
+        def _testPost02(*args, **kwargs):
+            return _testPost(*args, attendee="ATTENDEE:mailto:user02 at example.com", **kwargs)
+
+        def _testPost03(*args, **kwargs):
+            return _testPost(*args, attendee="ATTENDEE:mailto:user03 at example.com", **kwargs)
+
+        @inlineCallbacks
+        def _testPut(*args, **kwargs):
+            expectedResponseCode, method, url, headers, body = args
+            self.assertEqual(expectedResponseCode, CREATED)
+            self.assertEqual(method, 'PUT')
+            self.assertEqual(url, 'http://127.0.0.1/mumble/frotz.ics')
+            self.assertIsInstance(url, str)
+            self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+            consumer = MemoryConsumer()
+            yield body.startProducing(consumer)
+            self.assertEqual(
+                Component.fromString(consumer.value()),
+                Component.fromString(EVENT_INVITE))
+
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), CREATED, "Created", Headers({}),
+                StringProducer(""))
+
+            returnValue(response)
+
+        requests = [_testReport, _testPost02, _testReport, _testPost03, _testPut, ]
+
+        def _requestHandler(*args, **kwargs):
+            handler = requests.pop(0)
+            return handler(*args, **kwargs)
+        self.client._request = _requestHandler
+        yield self.client.addInvite('/mumble/frotz.ics', vcalendar)
+
+
+    def test_deleteEvent(self):
+        """
+        L{OS_X_10_6.deleteEvent} DELETEs the event at the relative
+        URL passed to it and updates local state to reflect its
+        removal.
+        """
+        requests = self.interceptRequests()
+
+        calendar = Calendar(caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/foo/', None)
+        event = Event(None, calendar.url + u'bar.ics', None)
+        self.client._calendars[calendar.url] = calendar
+        self.client._setEvent(event.url, event)
+
+        d = self.client.deleteEvent(event.url)
+
+        result, req = requests.pop()
+
+        expectedResponseCode, method, url = req
+
+        self.assertEqual(expectedResponseCode, NO_CONTENT)
+        self.assertEqual(method, 'DELETE')
+        self.assertEqual(url, 'http://127.0.0.1' + event.url)
+        self.assertIsInstance(url, str)
+
+        self.assertNotIn(event.url, self.client._events)
+        self.assertNotIn(u'bar.ics', calendar.events)
+
+        response = MemoryResponse(
+            ('HTTP', '1', '1'), NO_CONTENT, "No Content", None,
+            StringProducer(""))
+        result.callback(response)
+        return d
+
+
+    def test_serialization(self):
+        """
+        L{OS_X_10_6.serialize} properly generates a JSON document.
+        """
+        clientPath = os.path.join(self.client.serializePath, "user91-OS_X_10.6")
+        self.assertFalse(os.path.exists(clientPath))
+        indexPath = os.path.join(clientPath, "index.json")
+        self.assertFalse(os.path.exists(indexPath))
+
+        cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:004f8e41-b071-4b30-bb3b-6aada4adcc10
+DTSTART:20120817T113000
+DTEND:20120817T114500
+DTSTAMP:20120815T154420Z
+SEQUENCE:2
+SUMMARY:Simple event
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        cal2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:00a79cad-857b-418e-a54a-340b5686d747
+DTSTART:20120817T113000
+DTEND:20120817T114500
+DTSTAMP:20120815T154420Z
+SEQUENCE:2
+SUMMARY:Simple event
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        events = (
+            Event(self.client.serializeLocation(), u'/home/calendar/1.ics', u'123.123', Component.fromString(cal1)),
+            Event(self.client.serializeLocation(), u'/home/inbox/i1.ics', u'123.123', Component.fromString(cal2)),
+        )
+        self.client._events.update(dict([[event.url, event] for event in events]))
+
+        calendars = (
+            Calendar(str(caldavxml.calendar), set(('VEVENT',)), u'calendar', u'/home/calendar/', "123"),
+            Calendar(str(caldavxml.calendar), set(('VTODO',)), u'tasks', u'/home/tasks/', "456"),
+            Calendar(str(caldavxml.schedule_inbox), set(('VEVENT', "VTODO",)), u'calendar', u'/home/inbox/', "789"),
+        )
+        self.client._calendars.update(dict([[calendar.url, calendar] for calendar in calendars]))
+        self.client._calendars["/home/calendar/"].events["1.ics"] = events[0]
+        self.client._calendars["/home/inbox/"].events["i1.ics"] = events[1]
+
+        self.client.serialize()
+        self.assertTrue(os.path.exists(clientPath))
+        self.assertTrue(os.path.exists(indexPath))
+        def _normDict(d):
+            return dict([(k, sorted(v, key=lambda x: x["changeToken" if k == "calendars" else "url"]) if v else None,) for k, v in d.items()])
+        self.assertEqual(_normDict(json.loads(open(indexPath).read())), _normDict(json.loads("""{
+  "calendars": [
+    {
+      "changeToken": "123",
+      "name": "calendar",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+      "componentTypes": [
+        "VEVENT"
+      ],
+      "url": "/home/calendar/",
+      "events": [
+        "1.ics"
+      ]
+    },
+    {
+      "changeToken": "789",
+      "name": "calendar",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
+      "componentTypes": [
+        "VEVENT",
+        "VTODO"
+      ],
+      "url": "/home/inbox/",
+      "events": [
+        "i1.ics"
+      ]
+    },
+    {
+      "changeToken": "456",
+      "name": "tasks",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+      "componentTypes": [
+        "VTODO"
+      ],
+      "url": "/home/tasks/",
+      "events": []
+    }
+  ],
+  "principalURL": null,
+  "events": [
+    {
+      "url": "/home/calendar/1.ics",
+      "scheduleTag": null,
+      "etag": "123.123",
+      "uid": "004f8e41-b071-4b30-bb3b-6aada4adcc10"
+    },
+    {
+      "url": "/home/inbox/i1.ics",
+      "scheduleTag": null,
+      "etag": "123.123",
+      "uid": "00a79cad-857b-418e-a54a-340b5686d747"
+    }
+  ]
+}""")))
+
+        event1Path = os.path.join(clientPath, "calendar", "1.ics")
+        self.assertTrue(os.path.exists(event1Path))
+        self.assertEqual(open(event1Path).read(), cal1)
+
+        event2Path = os.path.join(clientPath, "inbox", "i1.ics")
+        self.assertTrue(os.path.exists(event2Path))
+        self.assertEqual(open(event2Path).read(), cal2)
+
+
+    def test_deserialization(self):
+        """
+        L{OS_X_10_6.deserailize} properly parses a JSON document.
+        """
+
+        cal1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:004f8e41-b071-4b30-bb3b-6aada4adcc10
+DTSTART:20120817T113000
+DTEND:20120817T114500
+DTSTAMP:20120815T154420Z
+SEQUENCE:2
+SUMMARY:Simple event
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        cal2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:00a79cad-857b-418e-a54a-340b5686d747
+DTSTART:20120817T113000
+DTEND:20120817T114500
+DTSTAMP:20120815T154420Z
+SEQUENCE:2
+SUMMARY:Simple event
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        clientPath = os.path.join(self.client.serializePath, "user91-OS_X_10.6")
+        os.mkdir(clientPath)
+        indexPath = os.path.join(clientPath, "index.json")
+        open(indexPath, "w").write("""{
+  "calendars": [
+    {
+      "changeToken": "321",
+      "name": "calendar",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+      "componentTypes": [
+        "VEVENT"
+      ],
+      "url": "/home/calendar/",
+      "events": [
+        "2.ics"
+      ]
+    },
+    {
+      "changeToken": "987",
+      "name": "calendar",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}schedule-inbox",
+      "componentTypes": [
+        "VEVENT",
+        "VTODO"
+      ],
+      "url": "/home/inbox/",
+      "events": [
+        "i2.ics"
+      ]
+    },
+    {
+      "changeToken": "654",
+      "name": "tasks",
+      "resourceType": "{urn:ietf:params:xml:ns:caldav}calendar",
+      "componentTypes": [
+        "VTODO"
+      ],
+      "url": "/home/tasks/",
+      "events": []
+    }
+  ],
+  "principalURL": null,
+  "events": [
+    {
+      "url": "/home/calendar/2.ics",
+      "scheduleTag": null,
+      "etag": "321.321",
+      "uid": "004f8e41-b071-4b30-bb3b-6aada4adcc10"
+    },
+    {
+      "url": "/home/inbox/i2.ics",
+      "scheduleTag": null,
+      "etag": "987.987",
+      "uid": "00a79cad-857b-418e-a54a-340b5686d747"
+    }
+  ]
+}""")
+
+        os.mkdir(os.path.join(clientPath, "calendar"))
+        event1Path = os.path.join(clientPath, "calendar", "2.ics")
+        open(event1Path, "w").write(cal1)
+        os.mkdir(os.path.join(clientPath, "inbox"))
+        event1Path = os.path.join(clientPath, "inbox", "i2.ics")
+        open(event1Path, "w").write(cal2)
+
+        self.client.deserialize()
+
+        self.assertEqual(len(self.client._calendars), 3)
+        self.assertTrue("/home/calendar/" in self.client._calendars)
+        self.assertEqual(self.client._calendars["/home/calendar/"].changeToken, "321")
+        self.assertEqual(self.client._calendars["/home/calendar/"].name, "calendar")
+        self.assertEqual(self.client._calendars["/home/calendar/"].resourceType, "{urn:ietf:params:xml:ns:caldav}calendar")
+        self.assertEqual(self.client._calendars["/home/calendar/"].componentTypes, set(("VEVENT",)))
+        self.assertTrue("/home/tasks/" in self.client._calendars)
+        self.assertTrue("/home/inbox/" in self.client._calendars)
+        self.assertEqual(self.client._calendars["/home/inbox/"].componentTypes, set(("VEVENT", "VTODO",)))
+        self.assertEqual(len(self.client._events), 2)
+        self.assertTrue("/home/calendar/2.ics" in self.client._events)
+        self.assertEqual(self.client._events["/home/calendar/2.ics"].scheduleTag, None)
+        self.assertEqual(self.client._events["/home/calendar/2.ics"].etag, "321.321")
+        self.assertEqual(self.client._events["/home/calendar/2.ics"].getUID(), "004f8e41-b071-4b30-bb3b-6aada4adcc10")
+        self.assertEqual(str(self.client._events["/home/calendar/2.ics"].component), cal1)
+        self.assertTrue("/home/inbox/i2.ics" in self.client._events)
+        self.assertEqual(self.client._events["/home/inbox/i2.ics"].scheduleTag, None)
+        self.assertEqual(self.client._events["/home/inbox/i2.ics"].etag, "987.987")
+        self.assertEqual(self.client._events["/home/inbox/i2.ics"].getUID(), "00a79cad-857b-418e-a54a-340b5686d747")
+        self.assertEqual(str(self.client._events["/home/inbox/i2.ics"].component), cal2)
+
+
+
+class UpdateCalendarTests(OS_X_10_6Mixin, TestCase):
+    """
+    Tests for L{OS_X_10_6._updateCalendar}.
+    """
+
+    _CALENDAR_PROPFIND_RESPONSE_BODY = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/anotherthing.ics</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <collection/>
+        </resourcetype>
+        <getetag>"None"</getetag>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+    <propstat>
+      <prop>
+      </prop>
+      <status>HTTP/1.1 404 Not Found</status>
+    </propstat>
+  </response>
+  <response>
+    <href>/something/else.ics</href>
+    <propstat>
+      <prop>
+        <resourcetype>
+          <collection/>
+        </resourcetype>
+        <getetag>"None"</getetag>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+   </response>
+</multistatus>
+"""
+    _CALENDAR_REPORT_RESPONSE_BODY = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/anotherthing.ics</href>
+    <status>HTTP/1.1 404 Not Found</status>
+  </response>
+  <response>
+    <href>/something/else.ics</href>
+    <propstat>
+      <prop>
+        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
+        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:CD54161A13AA8A4649D3781E at caldav.corp.apple.com
+DTSTART:20110715T140000Z
+DURATION:PT1H
+DTSTAMP:20110715T144217Z
+SUMMARY:Test2
+END:VEVENT
+END:VCALENDAR
+]]></calendar-data>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+  </response>
+</multistatus>
+"""
+
+    _CALENDAR_REPORT_RESPONSE_BODY_1 = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/anotherthing.ics</href>
+    <propstat>
+      <prop>
+        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
+        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:anotherthing at caldav.corp.apple.com
+DTSTART:20110715T140000Z
+DURATION:PT1H
+DTSTAMP:20110715T144217Z
+SUMMARY:Test1
+END:VEVENT
+END:VCALENDAR
+]]></calendar-data>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+  </response>
+</multistatus>
+"""
+
+    _CALENDAR_REPORT_RESPONSE_BODY_2 = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>/something/else.ics</href>
+    <propstat>
+      <prop>
+        <getetag>"ef70beb4cb7da4b2e2950350b09e9a01"</getetag>
+        <calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VEVENT
+UID:else at caldav.corp.apple.com
+DTSTART:20110715T140000Z
+DURATION:PT1H
+DTSTAMP:20110715T144217Z
+SUMMARY:Test2
+END:VEVENT
+END:VCALENDAR
+]]></calendar-data>
+      </prop>
+      <status>HTTP/1.1 200 OK</status>
+    </propstat>
+  </response>
+</multistatus>
+"""
+
+    def test_eventMissing(self):
+        """
+        If an event included in the calendar PROPFIND response no longer exists
+        by the time a REPORT is issued for that event, the 404 is handled and
+        the rest of the normal update logic for that event is skipped.
+        """
+        requests = self.interceptRequests()
+
+        calendar = Calendar(None, set(('VEVENT',)), 'calendar', '/something/', None)
+        self.client._calendars[calendar.url] = calendar
+        self.client._updateCalendar(calendar, "1234")
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
+        self.assertEqual('PROPFIND', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_PROPFIND_RESPONSE_BODY)))
+
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
+        self.assertEqual('REPORT', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
+
+        # Someone else comes along and gets rid of the event
+        del self.client._events["/something/anotherthing.ics"]
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY)))
+
+        # Verify that processing proceeded to the response after the one with a
+        # 404 status.
+        self.assertIn('/something/else.ics', self.client._events)
+
+
+    def test_multigetBatch(self):
+        """
+        If an event included in the calendar PROPFIND response no longer exists
+        by the time a REPORT is issued for that event, the 404 is handled and
+        the rest of the normal update logic for that event is skipped.
+        """
+        requests = self.interceptRequests()
+
+        self.patch(self.client, "MULTIGET_BATCH_SIZE", 1)
+
+        calendar = Calendar(None, set(('VEVENT',)), 'calendar', '/something/', None)
+        self.client._calendars[calendar.url] = calendar
+        self.client._updateCalendar(calendar, "1234")
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
+        self.assertEqual('PROPFIND', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_PROPFIND_RESPONSE_BODY)))
+
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
+        self.assertEqual('REPORT', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY_1)))
+
+        self.assertTrue(self.client._events['/something/anotherthing.ics'].etag is not None)
+        self.assertTrue(self.client._events['/something/else.ics'].etag is None)
+
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, _ignore_headers, _ignore_body = req
+        self.assertEqual('REPORT', method)
+        self.assertEqual('http://127.0.0.1/something/', url)
+        self.assertEqual((MULTI_STATUS,), expectedResponseCode)
+
+        result.callback(
+            MemoryResponse(
+                ('HTTP', '1', '1'), MULTI_STATUS, "Multi-status", None,
+                StringProducer(self._CALENDAR_REPORT_RESPONSE_BODY_2)))
+
+        self.assertTrue(self.client._events['/something/anotherthing.ics'].etag is not None)
+        self.assertTrue(self.client._events['/something/else.ics'].etag is not None)
+
+
+
+class VFreeBusyTests(OS_X_10_6Mixin, TestCase):
+    """
+    Tests for L{OS_X_10_6.requestAvailability}.
+    """
+    def test_requestAvailability(self):
+        """
+        L{OS_X_10_6.requestAvailability} accepts a date range and a set of
+        account uuids and issues a VFREEBUSY request.  It returns a Deferred
+        which fires with a dict mapping account uuids to availability range
+        information.
+        """
+        self.client.uuid = u'urn:uuid:user01'
+        self.client.email = u'mailto:user01 at example.com'
+        self.client.outbox = "/calendars/__uids__/%s/outbox/" % (self.record.uid,)
+        requests = self.interceptRequests()
+
+        start = DateTime(2011, 6, 10, 10, 45, 0, tzid=Timezone.UTCTimezone)
+        end = DateTime(2011, 6, 10, 11, 15, 0, tzid=Timezone.UTCTimezone)
+        d = self.client.requestAvailability(
+            start, end, [u"urn:uuid:user05", u"urn:uuid:user10"])
+
+        result, req = requests.pop(0)
+        expectedResponseCode, method, url, headers, body = req
+
+        self.assertEqual(OK, expectedResponseCode)
+        self.assertEqual('POST', method)
+        self.assertEqual(
+            'http://127.0.0.1/calendars/__uids__/%s/outbox/' % (self.record.uid,),
+            url)
+
+        self.assertEqual(headers.getRawHeaders('originator'), ['mailto:user01 at example.com'])
+        self.assertEqual(headers.getRawHeaders('recipient'), ['urn:uuid:user05, urn:uuid:user10'])
+        self.assertEqual(headers.getRawHeaders('content-type'), ['text/calendar'])
+
+        consumer = MemoryConsumer()
+        finished = body.startProducing(consumer)
+        def cbFinished(ignored):
+            vevent = Component.fromString(consumer.value())
+            uid = vevent.resourceUID()
+            dtstamp = vevent.mainComponent().propertyValue("DTSTAMP")
+            dtstamp = dtstamp.getText()
+            self.assertEqual("""BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VFREEBUSY
+UID:%(uid)s
+DTEND:20110611T000000Z
+ATTENDEE:urn:uuid:user05
+ATTENDEE:urn:uuid:user10
+DTSTART:20110610T000000Z
+DTSTAMP:%(dtstamp)s
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Availability for urn:uuid:user05, urn:uuid:user10
+END:VFREEBUSY
+END:VCALENDAR
+""".replace('\n', '\r\n') % {'uid': uid, 'dtstamp': dtstamp}, consumer.value())
+
+        finished.addCallback(cbFinished)
+
+        def requested(ignored):
+            response = MemoryResponse(
+                ('HTTP', '1', '1'), OK, "Ok", Headers({}),
+                StringProducer(""))
+            result.callback(response)
+        finished.addCallback(requested)
+
+        return d

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_logger.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_logger.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_logger.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,447 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+from StringIO import StringIO
+
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.logger import ReportStatistics, OperationLogger
+
+class ReportStatisticsTests(TestCase):
+    """
+    Tests for L{loadtest.population.ReportStatistics}.
+    """
+    def test_countUsers(self):
+        """
+        L{ReportStatistics.countUsers} returns the number of users observed to
+        have acted in the simulation.
+        """
+        logger = ReportStatistics()
+        users = ['user01', 'user02', 'user03']
+        for user in users:
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=1.23, user=user, client_type="test", client_id="1234"
+            ))
+        self.assertEqual(len(users), logger.countUsers())
+
+
+    def test_countClients(self):
+        """
+        L{ReportStatistics.countClients} returns the number of clients observed to
+        have acted in the simulation.
+        """
+        logger = ReportStatistics()
+        clients = ['c01', 'c02', 'c03']
+        for client in clients:
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=1.23, user="user01", client_type="test", client_id=client
+            ))
+        self.assertEqual(len(clients), logger.countClients())
+
+
+    def test_clientFailures(self):
+        """
+        L{ReportStatistics.countClientFailures} returns the number of clients observed to
+        have failed in the simulation.
+        """
+        logger = ReportStatistics()
+        clients = ['c01', 'c02', 'c03']
+        for client in clients:
+            logger.observe(dict(
+                type='client-failure', reason="testing %s" % (client,)
+            ))
+        self.assertEqual(len(clients), logger.countClientFailures())
+
+
+    def test_simFailures(self):
+        """
+        L{ReportStatistics.countSimFailures} returns the number of clients observed to
+        have caused an error in the simulation.
+        """
+        logger = ReportStatistics()
+        clients = ['c01', 'c02', 'c03']
+        for client in clients:
+            logger.observe(dict(
+                type='sim-failure', reason="testing %s" % (client,)
+            ))
+        self.assertEqual(len(clients), logger.countSimFailures())
+
+
+    def test_noFailures(self):
+        """
+        If fewer than 1% of requests fail, fewer than 1% of requests take 5
+        seconds or more, and fewer than 5% of requests take 3 seconds or more,
+        L{ReportStatistics.failures} returns an empty list.
+        """
+        logger = ReportStatistics()
+        logger.observe(dict(
+            type='response', method='GET', success=True,
+            duration=2.5, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual([], logger.failures())
+
+
+    def test_requestFailures(self):
+        """
+        If more than 1% of requests fail, L{ReportStatistics.failures} returns a
+        list containing a string describing this.
+        """
+        logger = ReportStatistics()
+        for _ignore in range(98):
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=2.5, user='user01', client_type="test", client_id="1234"
+            ))
+        logger.observe(dict(
+            type='response', method='GET', success=False,
+            duration=2.5, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 1% GET failed"],
+            logger.failures())
+
+
+    def test_threeSecondFailure(self):
+        """
+        If more than 5% of requests take longer than 3 seconds,
+        L{ReportStatistics.failures} returns a list containing a string
+        describing that.
+        """
+        logger = ReportStatistics()
+        for _ignore in range(94):
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=2.5, user='user01', client_type="test", client_id="1234"
+            ))
+        for _ignore in range(5):
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=3.5, user='user02', client_type="test", client_id="1234"
+            ))
+        self.assertEqual(
+            ["Greater than 5% GET exceeded 3 second response time"],
+            logger.failures())
+
+
+    def test_fiveSecondFailure(self):
+        """
+        If more than 1% of requests take longer than 5 seconds,
+        L{ReportStatistics.failures} returns a list containing a string
+        describing that.
+        """
+        logger = ReportStatistics()
+        for _ignore in range(98):
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=2.5, user='user01', client_type="test", client_id="1234"
+            ))
+        logger.observe(dict(
+            type='response', method='GET', success=True,
+            duration=5.5, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 1% GET exceeded 5 second response time"],
+            logger.failures())
+
+
+    def test_methodsCountedSeparately(self):
+        """
+        The counts for one method do not affect the results of another method.
+        """
+        logger = ReportStatistics()
+        for _ignore in range(99):
+            logger.observe(dict(
+                type='response', method='GET', success=True,
+                duration=2.5, user='user01', client_type="test", client_id="1234"
+            ))
+            logger.observe(dict(
+                type='response', method='POST', success=True,
+                duration=2.5, user='user01', client_type="test", client_id="1234"
+            ))
+
+        logger.observe(dict(
+            type='response', method='GET', success=False,
+            duration=2.5, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='POST', success=False,
+            duration=2.5, user='user01', client_type="test", client_id="1234"
+        ))
+
+        self.assertEqual([], logger.failures())
+
+
+    def test_bucketRequest(self):
+        """
+        PUT(xxx-huge/large/medium/small} have different thresholds. Test that requests straddling
+        each of those are correctly determined to be failures or not.
+        """
+
+        _thresholds = {
+            "requests": {
+                "limits": [0.1, 0.5, 1.0, 3.0, 5.0, 10.0, 30.0],
+                "thresholds": {
+                    "default": [100.0, 100.0, 100.0, 5.0, 1.0, 0.5, 0.0],
+                    "PUT{organizer-small}": [100.0, 50.0, 25.0, 5.0, 1.0, 0.5, 0.0],
+                    "PUT{organizer-medium}": [100.0, 100.0, 50.0, 25.0, 5.0, 1.0, 0.5],
+                    "PUT{organizer-large}": [100.0, 100.0, 100.0, 50.0, 25.0, 5.0, 1.0],
+                    "PUT{organizer-huge}": [100.0, 100.0, 100.0, 100.0, 100.0, 50.0, 25.0],
+                }
+            }
+        }
+
+        # -small below threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual([], logger.failures())
+
+        # -small above 0.5 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-small}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 50% PUT{organizer-small} exceeded 0.5 second response time"],
+            logger.failures()
+        )
+
+        # -medium below 0.5 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=0.6, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            [],
+            logger.failures()
+        )
+
+        # -medium above 1.0 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-medium}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 50% PUT{organizer-medium} exceeded 1 second response time"],
+            logger.failures()
+        )
+
+        # -large below 1.0 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=1.6, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            [],
+            logger.failures()
+        )
+
+        # -large above 3.0 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=0.2, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=3.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=3.6, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-large}', success=True,
+            duration=3.6, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 50% PUT{organizer-large} exceeded 3 second response time"],
+            logger.failures()
+        )
+
+        # -huge below 10.0 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=12.0, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=8, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=11.0, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=9.0, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            [],
+            logger.failures()
+        )
+
+        # -huge above 10.0 threshold
+        logger = ReportStatistics(thresholds=_thresholds)
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=12.0, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=9.0, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=12.0, user='user01', client_type="test", client_id="1234"
+        ))
+        logger.observe(dict(
+            type='response', method='PUT{organizer-huge}', success=True,
+            duration=42.42, user='user01', client_type="test", client_id="1234"
+        ))
+        self.assertEqual(
+            ["Greater than 50% PUT{organizer-huge} exceeded 10 second response time"],
+            logger.failures()
+        )
+
+
+
+class OperationLoggerTests(TestCase):
+    """
+    Tests for L{OperationLogger}.
+    """
+    def test_noFailures(self):
+        """
+        If the median lag is below 1 second and the failure rate is below 1%,
+        L{OperationLogger.failures} returns an empty list.
+        """
+        logger = OperationLogger(outfile=StringIO())
+        logger.observe(dict(
+            type='operation', phase='start', user='user01',
+            label='testing', lag=0.5)
+        )
+        logger.observe(dict(
+            type='operation', phase='end', user='user01',
+            duration=0.35, label='testing', success=True)
+        )
+        self.assertEqual([], logger.failures())
+
+
+    def test_lagLimitExceeded(self):
+        """
+        If the median scheduling lag for any operation in the simulation
+        exceeds 1 second, L{OperationLogger.failures} returns a list containing
+        a string describing that issue.
+        """
+        logger = OperationLogger(outfile=StringIO())
+        for lag in [100.0, 1100.0, 1200.0]:
+            logger.observe(dict(
+                type='operation', phase='start', user='user01',
+                label='testing', lag=lag)
+            )
+        self.assertEqual(
+            ["Median TESTING scheduling lag greater than 1000.0ms"],
+            logger.failures())
+
+
+    def test_failureLimitExceeded(self):
+        """
+        If the failure rate for any operation exceeds 1%,
+        L{OperationLogger.failures} returns a list containing a string
+        describing that issue.
+        """
+        logger = OperationLogger(outfile=StringIO())
+        for _ignore in range(98):
+            logger.observe(dict(
+                type='operation', phase='end', user='user01',
+                duration=0.25, label='testing', success=True)
+            )
+        logger.observe(dict(
+            type='operation', phase='end', user='user01',
+            duration=0.25, label='testing', success=False)
+        )
+        self.assertEqual(
+            ["Greater than 1% TESTING failed"],
+            logger.failures())

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_population.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_population.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_population.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,21 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+"""
+Tests for some things in L{loadtest.population}.
+"""
+

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_profiles.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_profiles.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_profiles.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,1029 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+"""
+Tests for loadtest.profiles.
+"""
+
+from caldavclientlibrary.protocol.caldav.definitions import caldavxml, csxml
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.task import Clock
+from twisted.internet.defer import succeed, fail
+from twisted.web.http import NO_CONTENT, PRECONDITION_FAILED
+from twisted.web.client import Response
+
+from twistedcaldav.ical import Component, Property
+
+from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
+from contrib.performance.loadtest.profiles import RealisticInviter
+from contrib.performance.loadtest.population import Populator, CalendarClientSimulator
+from contrib.performance.loadtest.ical import IncorrectResponseCode, Calendar, Event, BaseClient
+from contrib.performance.loadtest.sim import _DirectoryRecord
+
+import os
+
+SIMPLE_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0500
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:EDT
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:EST
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20101018T155431Z
+UID:C98AD237-55AD-4F7D-9009-0D355D835822
+DTEND;TZID=America/New_York:20101021T130000
+TRANSP:OPAQUE
+SUMMARY:Simple event
+DTSTART;TZID=America/New_York:20101021T120000
+DTSTAMP:20101018T155438Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+"""
+
+INVITED_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
+DTSTART;TZID=America/New_York:20110131T130000
+DTEND;TZID=America/New_York:20110131T140000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=NE
+ EDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:user02
+CREATED:20110124T170357Z
+DTSTAMP:20110124T170425Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:3
+SUMMARY:Some Event For You
+TRANSP:TRANSPARENT
+X-APPLE-NEEDS-REPLY:TRUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+ACCEPTED_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
+DTSTART;TZID=America/New_York:20110131T130000
+DTEND;TZID=America/New_York:20110131T140000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user02
+CREATED:20110124T170357Z
+DTSTAMP:20110124T170425Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:3
+SUMMARY:Some Event For You
+TRANSP:TRANSPARENT
+X-APPLE-NEEDS-REPLY:TRUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+INBOX_REPLY = """\
+BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+
+class AnyUser(object):
+    def __getitem__(self, index):
+        return _AnyRecord(index)
+
+
+
+class _AnyRecord(object):
+    def __init__(self, index):
+        self.uid = u"user%02d" % (index,)
+        self.password = u"user%02d" % (index,)
+        self.commonName = u"User %02d" % (index,)
+        self.email = u"user%02d at example.com" % (index,)
+        self.guid = u"user%02d" % (index,)
+
+
+
+class Deterministic(object):
+    def __init__(self, value=None):
+        self.value = value
+
+
+    def gauss(self, mean, stddev):
+        """
+        Pretend to return a value from a gaussian distribution with mu
+        parameter C{mean} and sigma parameter C{stddev}.  But actually
+        always return C{mean + 1}.
+        """
+        return mean + 1
+
+
+    def choice(self, sequence):
+        return sequence[0]
+
+
+    def sample(self):
+        return self.value
+
+
+
+class StubClient(BaseClient):
+    """
+    Stand in for an iCalendar client.
+
+    @ivar rescheduled: A set of event URLs which will not allow
+        attendee changes due to a changed schedule tag.
+    @ivar _pendingFailures: dict mapping URLs to failure objects
+    """
+    def __init__(self, number, serializePath):
+        self.serializePath = serializePath
+        os.mkdir(self.serializePath)
+        self.title = "StubClient"
+        self._events = {}
+        self._calendars = {}
+        self._pendingFailures = {}
+        self.record = _DirectoryRecord(
+            "user%02d" % (number,), "user%02d" % (number,),
+            "User %02d" % (number,), "user%02d at example.org" % (number,),
+            "user%02d" % (number,))
+        self.email = "mailto:user%02d at example.com" % (number,)
+        self.uuid = "urn:uuid:user%02d" % (number,)
+        self.rescheduled = set()
+        self.started = True
+
+
+    def _failDeleteWithObject(self, href, failureObject):
+        """
+        Accessor for inserting intentional failures for deletes.
+        """
+        self._pendingFailures[href] = failureObject
+
+
+    def serializeLocation(self):
+        """
+        Return the path to the directory where data for this user is serialized.
+        """
+        if self.serializePath is None or not os.path.isdir(self.serializePath):
+            return None
+
+        key = "%s-%s" % (self.record.uid, "StubClient")
+        path = os.path.join(self.serializePath, key)
+        if not os.path.exists(path):
+            os.mkdir(path)
+        elif not os.path.isdir(path):
+            return None
+
+        return path
+
+
+    def addEvent(self, href, vevent):
+        self._events[href] = Event(self.serializePath, href, None, vevent)
+        return succeed(None)
+
+
+    def addInvite(self, href, vevent):
+        return self.addEvent(href, vevent)
+
+
+    def deleteEvent(self, href,):
+        del self._events[href]
+        calendar, uid = href.rsplit('/', 1)
+        del self._calendars[calendar + '/'].events[uid]
+        if href in self._pendingFailures:
+            failureObject = self._pendingFailures.pop(href)
+            return fail(failureObject)
+        else:
+            return succeed(None)
+
+
+    def updateEvent(self, href):
+        self.rescheduled.remove(href)
+        return succeed(None)
+
+
+    def addEventAttendee(self, href, attendee):
+        vevent = self._events[href].component
+        vevent.mainComponent().addProperty(attendee)
+        self._events[href].component = vevent
+
+
+    def changeEventAttendee(self, href, old, new):
+        if href in self.rescheduled:
+            return fail(IncorrectResponseCode(
+                NO_CONTENT,
+                Response(
+                    ('HTTP', 1, 1), PRECONDITION_FAILED,
+                    'Precondition Failed', None, None))
+            )
+
+        vevent = self._events[href].component
+        vevent.mainComponent().removeProperty(old)
+        vevent.mainComponent().addProperty(new)
+        self._events[href].component = vevent
+        return succeed(None)
+
+
+    def _makeSelfAttendee(self):
+        attendee = Property(
+            name=u'ATTENDEE',
+            value=self.email,
+            params={
+                'CN': self.record.commonName,
+                'CUTYPE': 'INDIVIDUAL',
+                'PARTSTAT': 'ACCEPTED',
+            },
+        )
+        return attendee
+
+
+    def _makeSelfOrganizer(self):
+        organizer = Property(
+            name=u'ORGANIZER',
+            value=self.email,
+            params={
+                'CN': self.record.commonName,
+            },
+        )
+        return organizer
+
+
+
+class SequentialDistribution(object):
+    def __init__(self, values):
+        self.values = values
+
+
+    def sample(self):
+        return self.values.pop(0)
+
+
+
+class InviterTests(TestCase):
+    """
+    Tests for loadtest.profiles.Inviter.
+    """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None, None, None)
+
+
+    def _simpleAccount(self, userNumber, eventText):
+        client = StubClient(userNumber, self.mktemp())
+
+        vevent = Component.fromString(eventText)
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/cal/', None)
+        client._calendars.update({calendar.url: calendar})
+
+        event = Event(client.serializeLocation(), calendar.url + u'1234.ics', None, vevent)
+
+        client._events.update({event.url: event})
+        calendar.events = {u'1234.ics': event}
+
+        return vevent, event, calendar, client
+
+
+    def test_enabled(self):
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+
+        inviter = Inviter(None, self.sim, client, userNumber, **{"enabled": False})
+        self.assertEqual(inviter.enabled, False)
+
+        inviter = Inviter(None, self.sim, client, userNumber, **{"enabled": True})
+        self.assertEqual(inviter.enabled, True)
+
+
+    def test_doNotAddAttendeeToInbox(self):
+        """
+        When the only calendar with any events is a schedule inbox, no
+        attempt is made to add attendees to an event on that calendar.
+        """
+        userNumber = 10
+        vevent, _ignore_event, calendar, client = self._simpleAccount(
+            userNumber, SIMPLE_EVENT)
+        calendar.resourceType = caldavxml.schedule_inbox
+        inviter = Inviter(None, self.sim, client, userNumber)
+        inviter._invite()
+        self.assertFalse(vevent.mainComponent().hasProperty('ATTENDEE'))
+
+
+    def test_doNotAddAttendeeToNoCalendars(self):
+        """
+        When there are no calendars and no events at all, the inviter
+        does nothing.
+        """
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+        inviter = Inviter(None, self.sim, client, userNumber)
+        inviter._invite()
+        self.assertEquals(client._events, {})
+        self.assertEquals(client._calendars, {})
+
+
+    def test_doNotAddAttendeeToUninitializedEvent(self):
+        """
+        When there is an L{Event} on a calendar but the details of the
+        event have not yet been retrieved, no attempt is made to add
+        invitees to that event.
+        """
+        userNumber = 19
+        _ignore_vevent, event, calendar, client = self._simpleAccount(
+            userNumber, SIMPLE_EVENT)
+        event.component = event.etag = event.scheduleTag = None
+        inviter = Inviter(None, self.sim, client, userNumber)
+        inviter._invite()
+        self.assertEquals(client._events, {event.url: event})
+        self.assertEquals(client._calendars, {calendar.url: calendar})
+
+
+    def test_addAttendeeToEvent(self):
+        """
+        When there is a normal calendar with an event, inviter adds an
+        attendee to it.
+        """
+        userNumber = 16
+        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
+            userNumber, SIMPLE_EVENT)
+        inviter = Inviter(Clock(), self.sim, client, userNumber)
+        inviter.setParameters(inviteeDistribution=Deterministic(1))
+        inviter._invite()
+        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 1)
+        for paramname, paramvalue in {
+            'CN': 'User %d' % (userNumber + 1,),
+            'CUTYPE': 'INDIVIDUAL',
+            'PARTSTAT': 'NEEDS-ACTION',
+            'ROLE': 'REQ-PARTICIPANT',
+            'RSVP': 'TRUE'
+        }.items():
+            self.assertTrue(attendees[0].hasParameter(paramname))
+            self.assertEqual(attendees[0].parameterValue(paramname), paramvalue)
+
+
+    def test_doNotAddSelfToEvent(self):
+        """
+        If the inviter randomly selects its own user to be added to
+        the attendee list, a different user is added instead.
+        """
+        selfNumber = 12
+        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
+            selfNumber, SIMPLE_EVENT)
+
+        otherNumber = 20
+        values = [selfNumber - selfNumber, otherNumber - selfNumber]
+
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(inviteeDistribution=SequentialDistribution(values))
+        inviter._invite()
+        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 1)
+        for paramname, paramvalue in {
+            'CN': 'User %d' % (otherNumber,),
+            'CUTYPE': 'INDIVIDUAL',
+            'PARTSTAT': 'NEEDS-ACTION',
+            'ROLE': 'REQ-PARTICIPANT',
+            'RSVP': 'TRUE'
+        }.items():
+            self.assertTrue(attendees[0].hasParameter(paramname))
+            self.assertEqual(attendees[0].parameterValue(paramname), paramvalue)
+
+
+    def test_doNotAddExistingToEvent(self):
+        """
+        If the inviter randomly selects a user which is already an
+        invitee on the event, a different user is added instead.
+        """
+        selfNumber = 1
+        _ignore_vevent, event, _ignore_calendar, client = self._simpleAccount(
+            selfNumber, INVITED_EVENT)
+
+        invitee = tuple(event.component.mainComponent().properties('ATTENDEE'))[0]
+        inviteeNumber = int(invitee.parameterValue('CN').split()[1])
+        anotherNumber = inviteeNumber + 5
+        values = [inviteeNumber - selfNumber, anotherNumber - selfNumber]
+
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(inviteeDistribution=SequentialDistribution(values))
+        inviter._invite()
+        attendees = tuple(event.component.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 3)
+        for paramname, paramvalue in {
+            'CN': 'User %02d' % (anotherNumber,),
+            'CUTYPE': 'INDIVIDUAL',
+            'PARTSTAT': 'NEEDS-ACTION',
+            'ROLE': 'REQ-PARTICIPANT',
+            'RSVP': 'TRUE'
+        }.items():
+            self.assertTrue(attendees[2].hasParameter(paramname))
+            self.assertEqual(attendees[2].parameterValue(paramname), paramvalue)
+
+
+    def test_everybodyInvitedAlready(self):
+        """
+        If the first so-many randomly selected users we come across
+        are already attendees on the event, the invitation attempt is
+        abandoned.
+        """
+        selfNumber = 1
+        vevent, _ignore_event, _ignore_calendar, client = self._simpleAccount(
+            selfNumber, INVITED_EVENT)
+        inviter = Inviter(Clock(), self.sim, client, selfNumber)
+        # Always return a user number which has already been invited.
+        inviter.setParameters(inviteeDistribution=Deterministic(2 - selfNumber))
+        inviter._invite()
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 2)
+
+
+    def test_doNotInviteToSomeoneElsesEvent(self):
+        """
+        If there are events on our calendar which are being organized
+        by someone else, the inviter does not attempt to invite new
+        users to them.
+        """
+        selfNumber = 2
+        vevent, _ignore_event, _ignore_calendar, client = self._simpleAccount(
+            selfNumber, INVITED_EVENT)
+        inviter = Inviter(None, self.sim, client, selfNumber)
+        # Try to send an invitation, but with only one event on the
+        # calendar, of which we are not the organizer.  It should be
+        # unchanged afterwards.
+        inviter._invite()
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        self.assertEqual(len(attendees), 2)
+        self.assertEqual(attendees[0].parameterValue('CN'), 'User 01')
+        self.assertEqual(attendees[1].parameterValue('CN'), 'User 02')
+
+
+
+class RealisticInviterTests(TestCase):
+    """
+    Tests for loadtest.profiles.RealisticInviter.
+    """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None, None, None)
+
+
+    def _simpleAccount(self, userNumber, eventText):
+        client = StubClient(userNumber, self.mktemp())
+        vevent = Component.fromString(eventText)
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', u'/cal/', None)
+        event = Event(client.serializeLocation(), calendar.url + u'1234.ics', None, vevent)
+        calendar.events = {u'1234.ics': event}
+        client._events.update({event.url: event})
+        client._calendars.update({calendar.url: calendar})
+
+        return vevent, event, calendar, client
+
+
+    def test_enabled(self):
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+
+        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": False})
+        self.assertEqual(inviter.enabled, False)
+
+        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": True})
+        self.assertEqual(inviter.enabled, True)
+
+
+    def test_doNotAddInviteToInbox(self):
+        """
+        When the only calendar with any events is a schedule inbox, no
+        attempt is made to add attendees to that calendar.
+        """
+        calendar = Calendar(
+            caldavxml.schedule_inbox, set(), u'inbox', u'/sched/inbox', None)
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+
+        inviter = RealisticInviter(None, self.sim, client, userNumber, **{"enabled": False})
+        inviter._invite()
+
+        self.assertEquals(client._events, {})
+
+
+    def test_doNotAddInviteToNoCalendars(self):
+        """
+        When there are no calendars and no events at all, the inviter
+        does nothing.
+        """
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+        inviter = RealisticInviter(None, self.sim, client, userNumber)
+        inviter._invite()
+        self.assertEquals(client._events, {})
+        self.assertEquals(client._calendars, {})
+
+
+    def test_addInvite(self):
+        """
+        When there is a normal calendar, inviter adds an invite to it.
+        """
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
+        userNumber = 16
+        serializePath = self.mktemp()
+        os.mkdir(serializePath)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+        inviter = RealisticInviter(Clock(), self.sim, client, userNumber)
+        inviter.setParameters(
+            inviteeDistribution=Deterministic(1),
+            inviteeCountDistribution=Deterministic(1)
+        )
+        inviter._invite()
+        self.assertEquals(len(client._events), 1)
+        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
+        expected = set(("mailto:user%02d at example.com" % (userNumber,), "mailto:user%02d at example.com" % (userNumber + 1,),))
+        for attendee in attendees:
+            expected.remove(attendee.value())
+        self.assertEqual(len(expected), 0)
+
+
+    def test_doNotAddSelfToEvent(self):
+        """
+        If the inviter randomly selects its own user to be added to
+        the attendee list, a different user is added instead.
+        """
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
+        selfNumber = 12
+        client = StubClient(selfNumber, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+
+        otherNumber = 20
+        values = [selfNumber - selfNumber, otherNumber - selfNumber]
+
+        inviter = RealisticInviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(
+            inviteeDistribution=SequentialDistribution(values),
+            inviteeCountDistribution=Deterministic(1)
+        )
+        inviter._invite()
+        self.assertEquals(len(client._events), 1)
+        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
+        expected = set(("mailto:user%02d at example.com" % (selfNumber,), "mailto:user%02d at example.com" % (otherNumber,),))
+        for attendee in attendees:
+            expected.remove(attendee.value())
+        self.assertEqual(len(expected), 0)
+
+
+    def test_doNotAddExistingToEvent(self):
+        """
+        If the inviter randomly selects a user which is already an
+        invitee on the event, a different user is added instead.
+        """
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
+        selfNumber = 1
+        client = StubClient(selfNumber, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+
+        inviteeNumber = 20
+        anotherNumber = inviteeNumber + 5
+        values = [inviteeNumber - selfNumber, inviteeNumber - selfNumber, anotherNumber - selfNumber]
+
+        inviter = RealisticInviter(Clock(), self.sim, client, selfNumber)
+        inviter.setParameters(
+            inviteeDistribution=SequentialDistribution(values),
+            inviteeCountDistribution=Deterministic(2)
+        )
+        inviter._invite()
+        self.assertEquals(len(client._events), 1)
+        attendees = tuple(client._events.values()[0].component.mainComponent().properties('ATTENDEE'))
+        expected = set((
+            "mailto:user%02d at example.com" % (selfNumber,),
+            "mailto:user%02d at example.com" % (inviteeNumber,),
+            "mailto:user%02d at example.com" % (anotherNumber,),
+        ))
+        for attendee in attendees:
+            expected.remove(attendee.value())
+        self.assertEqual(len(expected), 0)
+
+
+    def test_everybodyInvitedAlready(self):
+        """
+        If the first so-many randomly selected users we come across
+        are already attendees on the event, the invitation attempt is
+        abandoned.
+        """
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
+        userNumber = 1
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+        inviter = RealisticInviter(Clock(), self.sim, client, userNumber)
+        inviter.setParameters(
+            inviteeDistribution=Deterministic(1),
+            inviteeCountDistribution=Deterministic(2)
+        )
+        inviter._invite()
+        self.assertEquals(len(client._events), 0)
+
+
+
+class AccepterTests(TestCase):
+    """
+    Tests for loadtest.profiles.Accepter.
+    """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None, None, None)
+
+
+    def test_enabled(self):
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+
+        accepter = Accepter(None, self.sim, client, userNumber, **{"enabled": False})
+        self.assertEqual(accepter.enabled, False)
+
+        accepter = Accepter(None, self.sim, client, userNumber, **{"enabled": True})
+        self.assertEqual(accepter.enabled, True)
+
+
+    def test_ignoreEventOnUnknownCalendar(self):
+        """
+        If an event on an unknown calendar changes, it is ignored.
+        """
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+        accepter = Accepter(None, self.sim, client, userNumber)
+        accepter.eventChanged('/some/calendar/1234.ics')
+
+
+    def test_ignoreNonCalendar(self):
+        """
+        If an event is on a calendar which is not of type
+        {CALDAV:}calendar, it is ignored.
+        """
+        userNumber = 14
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            csxml.dropbox_home, set(), u'notification', calendarURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[calendarURL] = calendar
+        accepter = Accepter(None, self.sim, client, userNumber)
+        accepter.eventChanged(calendarURL + '1234.ics')
+
+
+    def test_ignoreAccepted(self):
+        """
+        If the client is an attendee on an event but the PARTSTAT is
+        not NEEDS-ACTION, the event is ignored.
+        """
+        vevent = Component.fromString(ACCEPTED_EVENT)
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[calendarURL] = calendar
+        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
+        client._events[event.url] = event
+        accepter = Accepter(None, self.sim, client, userNumber)
+        accepter.eventChanged(event.url)
+
+
+    def test_ignoreAlreadyAccepting(self):
+        """
+        If the client sees an event change a second time before
+        responding to an invitation found on it during the first
+        change notification, the second change notification does not
+        generate another accept attempt.
+        """
+        clock = Clock()
+        randomDelay = 7
+        vevent = Component.fromString(INVITED_EVENT)
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[calendarURL] = calendar
+        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
+        client._events[event.url] = event
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.random = Deterministic()
+
+        def _gauss(mu, sigma):
+            return randomDelay
+        accepter.random.gauss = _gauss
+        accepter.eventChanged(event.url)
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+
+    def test_inboxReply(self):
+        """
+        When an inbox item that contains a reply is seen by the client, it
+        deletes it immediately.
+        """
+        userNumber = 1
+        clock = Clock()
+        inboxURL = '/some/inbox/'
+        vevent = Component.fromString(INBOX_REPLY)
+        inbox = Calendar(
+            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[inboxURL] = inbox
+
+        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
+        client._setEvent(inboxEvent.url, inboxEvent)
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.eventChanged(inboxEvent.url)
+        clock.advance(3)
+        self.assertNotIn(inboxEvent.url, client._events)
+        self.assertNotIn('4321.ics', inbox.events)
+
+
+    def test_inboxReplyFailedDelete(self):
+        """
+        When an inbox item that contains a reply is seen by the client, it
+        deletes it immediately.  If the delete fails, the appropriate response
+        code is returned.
+        """
+        userNumber = 1
+        clock = Clock()
+        inboxURL = '/some/inbox/'
+        vevent = Component.fromString(INBOX_REPLY)
+        inbox = Calendar(
+            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[inboxURL] = inbox
+
+        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
+        client._setEvent(inboxEvent.url, inboxEvent)
+        client._failDeleteWithObject(inboxEvent.url, IncorrectResponseCode(
+            NO_CONTENT,
+            Response(
+                ('HTTP', 1, 1), PRECONDITION_FAILED,
+                'Precondition Failed', None, None))
+        )
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.eventChanged(inboxEvent.url)
+        clock.advance(3)
+        self.assertNotIn(inboxEvent.url, client._events)
+        self.assertNotIn('4321.ics', inbox.events)
+
+
+    def test_acceptInvitation(self):
+        """
+        If the client is an attendee on an event and the PARTSTAT is
+        NEEDS-ACTION, a response is generated which accepts the
+        invitation and the corresponding event in the
+        I{schedule-inbox} is deleted.
+        """
+        clock = Clock()
+        randomDelay = 7
+        vevent = Component.fromString(INVITED_EVENT)
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
+        client = StubClient(userNumber, self.mktemp())
+
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
+        client._calendars[calendarURL] = calendar
+
+        inboxURL = '/some/inbox/'
+        inbox = Calendar(
+            caldavxml.schedule_inbox, set(), u'the inbox', inboxURL, None)
+        client._calendars[inboxURL] = inbox
+
+        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
+        client._setEvent(event.url, event)
+
+        inboxEvent = Event(client.serializeLocation(), inboxURL + u'4321.ics', None, vevent)
+        client._setEvent(inboxEvent.url, inboxEvent)
+
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+        vevent = client._events[event.url].component
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 2)
+        self.assertEquals(
+            attendees[1].parameterValue('CN'), 'User %02d' % (userNumber,))
+        self.assertEquals(
+            attendees[1].parameterValue('PARTSTAT'), 'ACCEPTED')
+        self.assertFalse(attendees[1].hasParameter('RSVP'))
+
+        self.assertNotIn(inboxEvent.url, client._events)
+        self.assertNotIn('4321.ics', inbox.events)
+
+
+    def test_reacceptInvitation(self):
+        """
+        If a client accepts an invitation on an event and then is
+        later re-invited to the same event, the invitation is again
+        accepted.
+        """
+        clock = Clock()
+        randomDelay = 7
+        vevent = Component.fromString(INVITED_EVENT)
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        userNumber = int(attendees[1].parameterValue('CN').split(None, 1)[1])
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
+        client = StubClient(userNumber, self.mktemp())
+        client._calendars[calendarURL] = calendar
+        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
+        client._events[event.url] = event
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+        # Now re-set the event so it has to be accepted again
+        event.component = Component.fromString(INVITED_EVENT)
+
+        # And now re-deliver it
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+        # And ensure that it was accepted again
+        vevent = client._events[event.url].component
+        attendees = tuple(vevent.mainComponent().properties('ATTENDEE'))
+        self.assertEquals(len(attendees), 2)
+        self.assertEquals(
+            attendees[1].parameterValue('CN'), 'User %02d' % (userNumber,))
+        self.assertEquals(
+            attendees[1].parameterValue('PARTSTAT'), 'ACCEPTED')
+        self.assertFalse(attendees[1].hasParameter('RSVP'))
+
+
+    def test_changeEventAttendeePreconditionFailed(self):
+        """
+        If the attempt to accept an invitation fails because of an
+        unmet precondition (412), the event is re-retrieved and the
+        PUT is re-issued with the new data.
+        """
+        clock = Clock()
+        userNumber = 2
+        client = StubClient(userNumber, self.mktemp())
+        randomDelay = 3
+
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'calendar', calendarURL, None)
+        client._calendars[calendarURL] = calendar
+
+        vevent = Component.fromString(INVITED_EVENT)
+        event = Event(client.serializeLocation(), calendarURL + u'1234.ics', None, vevent)
+        client._setEvent(event.url, event)
+
+        accepter = Accepter(clock, self.sim, client, userNumber)
+        accepter.setParameters(acceptDelayDistribution=Deterministic(randomDelay))
+
+        client.rescheduled.add(event.url)
+
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+
+
+class EventerTests(TestCase):
+    """
+    Tests for loadtest.profiles.Eventer, a profile which adds new
+    events on calendars.
+    """
+    def setUp(self):
+        self.sim = CalendarClientSimulator(
+            AnyUser(), Populator(None), None, None, None, None, None)
+
+
+    def test_enabled(self):
+        userNumber = 13
+        client = StubClient(userNumber, self.mktemp())
+
+        eventer = Eventer(None, self.sim, client, None, **{"enabled": False})
+        self.assertEqual(eventer.enabled, False)
+
+        eventer = Eventer(None, self.sim, client, None, **{"enabled": True})
+        self.assertEqual(eventer.enabled, True)
+
+
+    def test_doNotAddEventOnInbox(self):
+        """
+        When the only calendar is a schedule inbox, no attempt is made
+        to add events on it.
+        """
+        calendar = Calendar(
+            caldavxml.schedule_inbox, set(), u'inbox', u'/sched/inbox', None)
+        client = StubClient(21, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+
+        eventer = Eventer(None, self.sim, client, None)
+        eventer._addEvent()
+
+        self.assertEquals(client._events, {})
+
+
+    def test_addEvent(self):
+        """
+        When there is a normal calendar to add events to,
+        L{Eventer._addEvent} adds an event to it.
+        """
+        calendar = Calendar(
+            caldavxml.calendar, set(('VEVENT',)), u'personal stuff', u'/cals/personal', None)
+        client = StubClient(31, self.mktemp())
+        client._calendars.update({calendar.url: calendar})
+
+        eventer = Eventer(Clock(), self.sim, client, None)
+        eventer._addEvent()
+
+        self.assertEquals(len(client._events), 1)
+
+        # XXX Vary the event period/interval and the uid

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_pubsub.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_pubsub.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_pubsub.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,86 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+"""
+Tests for loadtest.pubsub
+"""
+from collections import defaultdict
+
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.pubsub import Publisher
+
+
+class PubSubTests(TestCase):
+    """Tests for Publisher"""
+    def setUp(self):
+        self.publisher = Publisher()
+        # Maps IDs of subscribers to values issued
+        self.values = defaultdict(list)
+
+
+    def recordValueWithID(self, id):
+        """
+        Return a one-argument callable for use as a subscriber
+        that appends its argument to the list of values associated with id
+        """
+        def recordValue(value):
+            self.values[id].append(value)
+        return recordValue
+
+
+    def test_noSubscribers(self):
+        """When no subscriptions are present, no value is published"""
+        value = 'foobar'
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [])
+
+
+    def test_subscribe(self):
+        """Published values propagate to their subscribers"""
+        value = 'foobar'
+        self.publisher.subscribe(self.recordValueWithID('sub1'))
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [value])
+
+
+    def test_cancel(self):
+        """Cancelled subscriptions do not receive new publications"""
+        value = 'foobar'
+        subscr = self.publisher.subscribe(self.recordValueWithID('sub1'))
+        subscr.cancel()
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [])
+
+
+    def test_multiple_subscriptions(self):
+        """Publisher supports adding and removing multiple subscriptions"""
+        value = 'foobar'
+        subscr1 = self.publisher.subscribe(self.recordValueWithID('sub1'))
+        subscr2 = self.publisher.subscribe(self.recordValueWithID('sub2'))
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [value])
+        self.assertEqual(self.values['sub2'], [value])
+
+        subscr2.cancel()
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [value, value])
+        self.assertEqual(self.values['sub2'], [value])
+
+        subscr1.cancel()
+        self.publisher.issue(value)
+        self.assertEqual(self.values['sub1'], [value, value])
+        self.assertEqual(self.values['sub2'], [value])

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_push.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_push.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_push.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,114 @@
+from twisted.trial.unittest import TestCase
+from twisted.internet.defer import inlineCallbacks
+
+from contrib.performance.loadtest.push import PushMonitor
+
+class PushMonitorTests(TestCase):
+    def sendFakePush(self, pushkey):
+        self.monitor._receivedAMPPush(inboundID=pushkey, dataChangedTimestamp=None, priority=None)
+
+    def receivedPush(self, calendar_href):
+        self.history.append(calendar_href)
+
+    def setUp(self):
+        """
+        Creates and begins a PushMonitor with a history-tracking callback
+        """
+
+        self.monitor = PushMonitor(
+            None,
+            'localhost',
+            62311,
+            self.receivedPush
+        )
+        self.history = []
+        return self.monitor.begin()
+
+    def test_noPushkey(self):
+        """
+        Monitor will not react to a push if there are no registered pushkeys
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
+
+    @inlineCallbacks
+    def test_addPushkey(self):
+        """
+        Adding a pushkey triggers a notification for the corresponding calendar home
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
+
+    @inlineCallbacks
+    def test_removePushkey(self):
+        """
+        Pushkeys can be unregistered
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.removePushkey(pushkey)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
+
+    @inlineCallbacks
+    def test_addDuplicatePushkeys(self):
+        """
+        Adding the same pushkey twice only registers it once
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
+
+    @inlineCallbacks
+    def test_addOverridePushkeys(self):
+        """
+        Adding the same pushkey with a different calendar home
+        unregisters the original
+        """
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey, calendar_home_2)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2])
+
+    @inlineCallbacks
+    def test_multiplePushkeys(self):
+        """
+        Monitor supports registering multiple pushkeys
+        """
+        pushkey = '/CalDAV/abc/def/'
+        pushkey_2 = '/CalDAV/abc/xyz/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey_2, calendar_home_2)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+        self.sendFakePush(pushkey_2)
+        self.assertEqual(self.history, [calendar_home_2])
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2, calendar_home])
+
+    def tearDown(self):
+        return self.monitor.end()

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_records.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_records.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_records.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,133 @@
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+"""
+Tests for loadtest.records
+"""
+
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.records import DirectoryRecord, recordsFromCSVFile
+
+class RecordTests(TestCase):
+    def test_loadAccountsFromFile(self):
+        """
+        L{LoadSimulator.fromCommandLine} takes an account loader from the
+        config file and uses it to create user records for use in the
+        simulation.
+        """
+        accounts = FilePath(self.mktemp())
+        accounts.setContent("foo,bar,baz,quux,goo\nfoo2,bar2,baz2,quux2,goo2\n")
+        config = VALID_CONFIG.copy()
+        config["accounts"] = {
+            "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
+            "params": {
+                "path": accounts.path
+            },
+        }
+        configpath = FilePath(self.mktemp())
+        configpath.setContent(writePlistToString(config))
+        io = StringIO()
+        sim = LoadSimulator.fromCommandLine(['--config', configpath.path], io)
+        self.assertEquals(io.getvalue(), "Loaded 2 accounts.\n")
+        self.assertEqual(2, len(sim.records))
+        self.assertEqual(sim.records[0].uid, 'foo')
+        self.assertEqual(sim.records[0].password, 'bar')
+        self.assertEqual(sim.records[0].commonName, 'baz')
+        self.assertEqual(sim.records[0].email, 'quux')
+        self.assertEqual(sim.records[1].uid, 'foo2')
+        self.assertEqual(sim.records[1].password, 'bar2')
+        self.assertEqual(sim.records[1].commonName, 'baz2')
+        self.assertEqual(sim.records[1].email, 'quux2')
+
+
+    def test_loadDefaultAccountsFromFile(self):
+        """
+        L{LoadSimulator.fromCommandLine} takes an account loader (with
+        empty path)from the config file and uses it to create user
+        records for use in the simulation.
+        """
+        config = VALID_CONFIG.copy()
+        config["accounts"] = {
+            "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
+            "params": {
+                "path": ""
+            },
+        }
+        configpath = FilePath(self.mktemp())
+        configpath.setContent(writePlistToString(config))
+        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
+                                            StringIO())
+        self.assertEqual(99, len(sim.records))
+        self.assertEqual(sim.records[0].uid, 'user01')
+        self.assertEqual(sim.records[0].password, 'user01')
+        self.assertEqual(sim.records[0].commonName, 'User 01')
+        self.assertEqual(sim.records[0].email, 'user01 at example.com')
+        self.assertEqual(sim.records[98].uid, 'user99')
+        self.assertEqual(sim.records[98].password, 'user99')
+        self.assertEqual(sim.records[98].commonName, 'User 99')
+        self.assertEqual(sim.records[98].email, 'user99 at example.com')
+
+
+ormance.loadtest.sim.generateRecords",
+            "params": {
+                "count": 2
+            },
+        }
+        configpath = FilePath(self.mktemp())
+        configpath.setContent(writePlistToString(config))
+        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
+                                            StringIO())
+        self.assertEqual(2, len(sim.records))
+        self.assertEqual(sim.records[0].uid, 'user1')
+        self.assertEqual(sim.records[0].password, 'user1')
+        self.assertEqual(sim.records[0].commonName, 'User 1')
+        self.assertEqual(sim.records[0].email, 'user1 at example.com')
+        self.assertEqual(sim.records[1].uid, 'user2')
+        self.assertEqual(sim.records[1].password, 'user2')
+        self.assertEqual(sim.records[1].commonName, 'User 2')
+        self.assertEqual(sim.records[1].email, 'user2 at example.com')
+
+
+    def test_generateRecordsNonDefaultPatterns(self):
+        """
+        L{LoadSimulator.fromCommandLine} takes an account loader from the
+        config file and uses it to generate user records for use in the
+        simulation.
+        """
+        config = VALID_CONFIG.copy()
+        config["accounts"] = {
+            "loader": "contrib.performance.loadtest.sim.generateRecords",
+            "params": {
+                "count": 3,
+                "uidPattern": "USER%03d",
+                "passwordPattern": "PASSWORD%03d",
+                "namePattern": "Test User %03d",
+                "emailPattern": "USER%03d at example2.com",
+            },
+        }
+        configpath = FilePath(self.mktemp())
+        configpath.setContent(writePlistToString(config))
+        sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
+                                            StringIO())
+        self.assertEqual(3, len(sim.records))
+        self.assertEqual(sim.records[0].uid, 'USER001')
+        self.assertEqual(sim.records[0].password, 'PASSWORD001')
+        self.assertEqual(sim.records[0].commonName, 'Test User 001')
+        self.assertEqual(sim.records[0].email, 'USER001 at example2.com')
+        self.assertEqual(sim.records[2].uid, 'USER003')
+        self.assertEqual(sim.records[2].password, 'PASSWORD003')
+        self.assertEqual(sim.records[2].commonName, 'Test User 003')
+        self.assertEqual(sim.records[2].email, 'USER003 at example2.com')
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_requester.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_requester.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_requester.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,48 @@
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.requester import (
+    Requester, IncorrectResponseCode, WebClientContextFactory
+)
+
+class _FakeAgent(object):
+    def __init__(self):
+        self.requests = []
+        self.next = None
+
+    def request(self, method, uri, headers=None, bodyProducer=None):
+        self.requests.append((method, uri, headers, bodyProducer))
+
+    def setNextResponse(response):
+
+
+    # {'reactor': <contrib.performance.loadtest.trafficlogger.(Logged Reactor) object at 0x10d1860d0>, 'title': '10.11 Intern', 'self': <contrib.performance.loadtest.requester.Requester object at 0x10d184a10>, 'auth': {'digest': <urllib2.HTTPDigestAuthHandler instance at 0x10d192128>, 'basic': <urllib2.HTTPBasicAuthHandler instance at 0x10d180fc8>}, 'headers': {'Connection': ['keep-alive'], 'Accept-Language': ['en-us'], 'Accept-Encoding': ['gzip,deflate'], 'Accept': ['*/*'], 'User-Agent': ['Mac+OS+X/10.11 (15A216g) CalendarAgent/353']}, 'client_id': 'a11bce96-f787-4096-91a3-44f7dbf38a06', 'root': 'https://127.0.0.1:8443', 'uid': u'user01'}
+    
+
+class RequesterTests(TestCase):
+    def setUp(self):
+        self.root = '/foo/bar/'
+        self.headers = Headers({
+            'Connection': ['keep-alive'],
+            'Accept-Language': ['en-us'],
+            'Accept-Encoding': ['gzip,deflate'],
+            'Accept': ['*/*'],
+            'User-Agent': ['Mac+OS+X/10.11 (15A216g) CalendarAgent/353'
+        }
+        self.title = 'Requester '
+
+    self,
+        root,
+        headers,
+        title,
+        uid,
+        client_id,
+        auth,
+        reactor
+
+    
+
+class IncorrectResponseCodeTests(TestCase):
+    pass
+
+class WebClientContextFactoryTests(TestCase):
+    pass
\ No newline at end of file

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_resources.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_resources.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_resources.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,39 @@
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.resources import Event, Calendar
+class EventTests(TestCase):
+    """
+    Tests for L{Event}.
+    """
+    def test_uid(self):
+        """
+        When the C{vevent} attribute of an L{Event} instance is set,
+        L{Event.getUID} returns the UID value from it.
+        """
+        event = Event(None, u'/foo/bar', u'etag', Component.fromString(EVENT))
+        self.assertEquals(event.getUID(), EVENT_UID)
+
+
+    def test_withoutUID(self):
+        """
+        When an L{Event} has a C{vevent} attribute set to C{None},
+        L{Event.getUID} returns C{None}.
+        """
+        event = Event(None, u'/bar/baz', u'etag')
+        self.assertIdentical(event.getUID(), None)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_sim.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_sim.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_sim.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,474 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+from plistlib import writePlistToString
+from cStringIO import StringIO
+
+from twisted.python.log import msg
+from twisted.python.usage import UsageError
+from twisted.python.filepath import FilePath
+from twisted.internet.defer import Deferred, succeed
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.distributions import NormalDistribution
+from contrib.performance.loadtest.ical import OS_X_10_6
+from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
+from contrib.performance.loadtest.population import (
+    SmoothRampUp, ClientType, PopulationParameters, Populator, CalendarClientSimulator,
+    ProfileType
+)
+from contrib.performance.loadtest.logger import SimpleStatistics
+from contrib.performance.loadtest.sim import (
+    Arrival, SimOptions, LoadSimulator, LagTrackingReactor,
+    _DirectoryRecord
+)
+
+
+VALID_CONFIG = {
+    'server': 'tcp:127.0.0.1:8008',
+    'webadmin': {
+        'enabled': True,
+        'HTTPPort': 8080,
+    },
+    'arrival': {
+        'factory': 'contrib.performance.loadtest.population.SmoothRampUp',
+        'params': {
+            'groups': 10,
+            'groupSize': 1,
+            'interval': 3,
+        },
+    },
+}
+
+VALID_CONFIG_PLIST = writePlistToString(VALID_CONFIG)
+
+
+
+class SimOptionsTests(TestCase):
+    def test_defaultConfig(self):
+        """
+        If the I{config} option is not specified, the default config.plist in
+        the source tree is used.
+        """
+        options = SimOptions()
+        self.assertEqual(options['config'], FilePath(__file__).sibling('config.plist'))
+
+
+    def test_configFileNotFound(self):
+        """
+        If the filename given to the I{config} option is not found,
+        L{SimOptions.parseOptions} raises a L{UsageError} indicating
+        this.
+        """
+        name = FilePath(self.mktemp())
+        options = SimOptions()
+        exc = self.assertRaises(
+            UsageError, options.parseOptions, ['--config', name.path])
+        self.assertEquals(
+            str(exc), "--config %s: No such file or directory" % (name.path,))
+
+
+    def test_configFileNotParseable(self):
+        """
+        If the contents of the file given to the I{config} option
+        cannot be parsed by L{ConfigParser},
+        L{SimOptions.parseOptions} raises a L{UsageError} indicating
+        this.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent("some random junk")
+        options = SimOptions()
+        exc = self.assertRaises(
+            UsageError, options.parseOptions, ['--config', config.path])
+        self.assertEquals(
+            str(exc),
+            "--config %s: syntax error: line 1, column 0" % (config.path,))
+
+
+
+class CalendarClientSimulatorTests(TestCase):
+    """
+    Tests for L{CalendarClientSimulator} which adds running clients to
+    a simulation.
+    """
+    realmName = 'stub'
+
+    def _user(self, name):
+        password = 'password-' + name
+        email = name + "@example.com"
+        record = _DirectoryRecord(name, password, name, email, name)
+        return record
+
+
+    def test_createUser(self):
+        """
+        Subsequent calls to L{CalendarClientSimulator._createUser}
+        with different user numbers return user details from different
+        directory records.
+        """
+        calsim = CalendarClientSimulator(
+            [self._user('alice'), self._user('bob'), self._user('carol')],
+            Populator(None), None, None, 'http://example.org:1234/', None, None)
+        users = sorted([
+            calsim._createUser(0)[0],
+            calsim._createUser(1)[0],
+            calsim._createUser(2)[0],
+        ])
+        self.assertEqual(['alice', 'bob', 'carol'], users)
+
+
+    def test_createUserAuthInfo(self):
+        """
+        The auth handler returned by L{CalendarClientSimulator._createUser}
+        includes the password taken from user's directory record.
+        """
+        calsim = CalendarClientSimulator(
+            [self._user('alice')],
+            Populator(None), None, None, 'http://example.org:1234/', None, None)
+        user, auth = calsim._createUser(0)
+        self.assertEqual(
+            auth['basic'].passwd.find_user_password('Test Realm', 'http://example.org:1234/')[1],
+            'password-' + user)
+        self.assertEqual(
+            auth['digest'].passwd.find_user_password('Test Realm', 'http://example.org:1234/')[1],
+            'password-' + user)
+
+
+    def test_stop(self):
+        """
+        After L{CalendarClientSimulator.stop} is called, failed clients and
+        profiles are not logged.
+        """
+        class BrokenClient(object):
+            def __init__(self, reactor, serverAddress, serializationPath, userInfo, auth, runResult):
+                self._runResult = runResult
+
+            def run(self):
+                return self._runResult
+
+            def stop(self):
+                return succeed(None)
+
+        class BrokenProfile(object):
+            def __init__(self, reactor, simulator, client, userNumber, runResult):
+                self._runResult = runResult
+                self.enabled = True
+
+            def initialize(self):
+                return succeed(None)
+
+            def run(self):
+                return self._runResult
+
+        clientRunResult = Deferred()
+        profileRunResult = Deferred()
+
+        params = PopulationParameters()
+        params.addClient(1, ClientType(
+            BrokenClient, {'runResult': clientRunResult},
+            [ProfileType(BrokenProfile, {'runResult': profileRunResult})])
+        )
+        sim = CalendarClientSimulator(
+            [self._user('alice')], Populator(None), params, None, 'http://example.com:1234/', None, None)
+        sim.add(1, 1)
+        sim.stop()
+        clientRunResult.errback(RuntimeError("Some fictional client problem"))
+        profileRunResult.errback(RuntimeError("Some fictional profile problem"))
+
+        self.assertEqual([], self.flushLoggedErrors())
+
+
+
+class Reactor(object):
+    message = "some event to be observed"
+
+    def __init__(self):
+        self._triggers = []
+        self._whenRunning = []
+
+
+    def run(self):
+        for thunk in self._whenRunning:
+            thunk()
+        msg(thingo=self.message)
+        for _ignore_phase, event, thunk in self._triggers:
+            if event == 'shutdown':
+                thunk()
+
+
+    def callWhenRunning(self, thunk):
+        self._whenRunning.append(thunk)
+
+
+    def addSystemEventTrigger(self, phase, event, thunk):
+        self._triggers.append((phase, event, thunk))
+
+
+
+class Observer(object):
+    def __init__(self):
+        self.reported = False
+        self.events = []
+
+
+    def observe(self, event):
+        self.events.append(event)
+
+
+    def report(self, output):
+        self.reported = True
+
+
+    def failures(self):
+        return []
+
+
+
+class NullArrival(object):
+    def run(self, sim):
+        pass
+
+
+
+class StubSimulator(LoadSimulator):
+    def run(self):
+        return 3
+
+
+
+class LoadSimulatorTests(TestCase):
+    def test_main(self):
+        """
+        L{LoadSimulator.main} raises L{SystemExit} with the result of
+        L{LoadSimulator.run}.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(VALID_CONFIG_PLIST)
+
+        exc = self.assertRaises(
+            SystemExit, StubSimulator.main, ['--config', config.path])
+        self.assertEquals(
+            exc.args, (StubSimulator(None, None, None, None, None, None, None).run(),))
+
+
+    def test_createSimulator(self):
+        """
+        L{LoadSimulator.createSimulator} creates a L{CalendarClientSimulator}
+        with its own reactor and host and port information from the
+        configuration file.
+        """
+        server = 'http://127.0.0.7:1243/'
+        reactor = object()
+        sim = LoadSimulator(server, None, None, None, None, None, None, reactor=reactor)
+        calsim = sim.createSimulator()
+        self.assertIsInstance(calsim, CalendarClientSimulator)
+        self.assertIsInstance(calsim.reactor, LagTrackingReactor)
+        self.assertIdentical(calsim.reactor._reactor, reactor)
+        self.assertEquals(calsim.server, server)
+
+
+    def test_specifyRuntime(self):
+        """
+        L{LoadSimulator.fromCommandLine} recognizes the I{--runtime} option to
+        specify a limit on how long the simulation will run.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(VALID_CONFIG_PLIST)
+        sim = LoadSimulator.fromCommandLine(['--config', config.path, '--runtime', '123'])
+        self.assertEqual(123, sim.runtime)
+
+
+    def test_loadServerConfig(self):
+        """
+        The Calendar Server host and port are loaded from the [server]
+        section of the configuration file specified.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(
+            writePlistToString({"server": "https://127.0.0.3:8432/"})
+        )
+        sim = LoadSimulator.fromCommandLine(['--config', config.path])
+        self.assertEquals(sim.server, "https://127.0.0.3:8432/")
+
+
+    def test_loadArrivalConfig(self):
+        """
+        The arrival policy type and arguments are loaded from the
+        [arrival] section of the configuration file specified.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(
+            writePlistToString({
+                "arrival": {
+                    "factory": "contrib.performance.loadtest.population.SmoothRampUp",
+                    "params": {
+                        "groups": 10,
+                        "groupSize": 1,
+                        "interval": 3,
+                    },
+                },
+            })
+        )
+        sim = LoadSimulator.fromCommandLine(['--config', config.path])
+        self.assertEquals(
+            sim.arrival,
+            Arrival(SmoothRampUp, dict(groups=10, groupSize=1, interval=3)))
+
+
+    def test_createArrivalPolicy(self):
+        """
+        L{LoadSimulator.createArrivalPolicy} creates an arrival
+        policy based on the L{Arrival} passed to its initializer.
+        """
+        class FakeArrival(object):
+            def __init__(self, reactor, x, y):
+                self.reactor = reactor
+                self.x = x
+                self.y = y
+
+        reactor = object()
+        sim = LoadSimulator(
+            None, None, None, None, None, Arrival(FakeArrival, {'x': 3, 'y': 2}), None, reactor=reactor)
+        arrival = sim.createArrivalPolicy()
+        self.assertIsInstance(arrival, FakeArrival)
+        self.assertIdentical(arrival.reactor, sim.reactor)
+        self.assertEquals(arrival.x, 3)
+        self.assertEquals(arrival.y, 2)
+
+
+    def test_loadPopulationParameters(self):
+        """
+        Client weights and profiles are loaded from the [clients]
+        section of the configuration file specified.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(
+            writePlistToString(
+                {
+                    "clients": [
+                        {
+                            "software": "contrib.performance.loadtest.ical.OS_X_10_6",
+                            "params": {
+                                "foo": "bar"
+                            },
+                            "profiles": [
+                                {
+                                    "params": {
+                                        "interval": 25,
+                                        "eventStartDistribution": {
+                                            "type": "contrib.performance.stats.NormalDistribution",
+                                            "params": {
+                                                "mu": 123,
+                                                "sigma": 456,
+                                            }
+                                        }
+                                    },
+                                    "class": "contrib.performance.loadtest.profiles.Eventer"
+                                }
+                            ],
+                            "weight": 3,
+                        }
+                    ]
+                }
+            )
+        )
+
+        sim = LoadSimulator.fromCommandLine(
+            ['--config', config.path, '--clients', config.path]
+        )
+        expectedParameters = PopulationParameters()
+        expectedParameters.addClient(
+            3,
+            ClientType(
+                OS_X_10_6,
+                {"foo": "bar"},
+                [
+                    ProfileType(
+                        Eventer, {
+                            "interval": 25,
+                            "eventStartDistribution": NormalDistribution(123, 456)
+                        }
+                    )
+                ]
+            )
+        )
+        self.assertEquals(sim.parameters, expectedParameters)
+
+
+    def test_requireClient(self):
+        """
+        At least one client is required, so if a configuration with an
+        empty clients array is specified, a single default client type
+        is used.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(writePlistToString({"clients": []}))
+        sim = LoadSimulator.fromCommandLine(
+            ['--config', config.path, '--clients', config.path]
+        )
+        expectedParameters = PopulationParameters()
+        expectedParameters.addClient(
+            1, ClientType(OS_X_10_6, {}, [Eventer, Inviter, Accepter]))
+        self.assertEquals(sim.parameters, expectedParameters)
+
+
+    def test_loadLogObservers(self):
+        """
+        Log observers specified in the [observers] section of the
+        configuration file are added to the logging system.
+        """
+        config = FilePath(self.mktemp())
+        config.setContent(
+            writePlistToString(
+                {
+                    "observers": [
+                        {
+                            "type": "contrib.performance.loadtest.logger.SimpleStatistics",
+                            "params": {},
+                        },
+                    ]
+                }
+            )
+        )
+        sim = LoadSimulator.fromCommandLine(['--config', config.path])
+        self.assertEquals(len(sim.observers), 1)
+        self.assertIsInstance(sim.observers[0], SimpleStatistics)
+
+
+    def test_observeRunReport(self):
+        """
+        Each log observer is added to the log publisher before the
+        simulation run is started and has its C{report} method called
+        after the simulation run completes.
+        """
+        observers = [Observer()]
+        sim = LoadSimulator(
+            "http://example.com:123/",
+            "/principals/users/%s/",
+            None,
+            None,
+            None,
+            Arrival(lambda reactor: NullArrival(), {}),
+            None, observers, reactor=Reactor())
+        io = StringIO()
+        sim.run(io)
+        self.assertEquals(io.getvalue(), "\n*** PASS\n")
+        self.assertTrue(observers[0].reported)
+        self.assertEquals(
+            [e for e in observers[0].events if "thingo" in e][0]["thingo"],
+            Reactor.message
+        )

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_templates.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_templates.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_templates.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,47 @@
+# -*- test-case-name: contrib.performance.loadtest.test_templates -*-
+##
+# Copyright (c) 2010-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+"""
+Tests for loadtest.templates
+"""
+
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.templates import eventTemplate, alarmTemplate, taskTemplate
+
+
+class TemplateTests(TestCase):
+    def assertVCalendar(self, vcal):
+        self.assertEqual(vcal.name(), 'VCALENDAR')
+        self.assertEqual(vcal.propertyValue('VERSION'), '2.0')
+
+    def test_eventTemplate(self):
+        self.assertVCalendar(eventTemplate)
+        vevent = eventTemplate.mainComponent()
+        self.assertEqual(vevent.name(), 'VEVENT')
+
+    def test_taskTemplate(self):
+        self.assertVCalendar(taskTemplate)
+        vtodo = taskTemplate.mainComponent()
+        self.assertEqual(vtodo.name(), 'VTODO')
+
+    def test_alarmTemplate(self):
+        self.assertVCalendar(alarmTemplate)
+        valarm = alarmTemplate.mainComponent()
+        self.assertEqual(valarm.name(), 'VALARM')
+        self.assertTrue(valarm.hasProperty('ACTION'))
+        self.assertTrue(valarm.hasProperty('TRIGGER'))

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_trafficlogger.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_trafficlogger.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_trafficlogger.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,210 @@
+##
+# Copyright (c) 2011-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+from zope.interface import Interface, implements
+
+from twisted.internet.protocol import ClientFactory, Protocol
+from twisted.trial.unittest import TestCase
+from twisted.test.proto_helpers import StringTransport, MemoryReactor
+from twisted.protocols.wire import Discard
+
+from contrib.performance.loadtest.trafficlogger import _TrafficLoggingFactory, loggedReactor
+
+
+class IProbe(Interface):
+    """
+    An interface which can be used to verify some interface-related behavior of
+    L{loggedReactor}.
+    """
+    def probe(): #@NoSelf
+        pass
+
+
+
+class Probe(object):
+    implements(IProbe)
+
+    _probed = False
+
+    def __init__(self, result=None):
+        self._result = result
+
+
+    def probe(self):
+        self._probed = True
+        return self._result
+
+
+
+class TrafficLoggingReactorTests(TestCase):
+    """
+    Tests for L{loggedReactor}.
+    """
+    def test_nothing(self):
+        """
+        L{loggedReactor} returns the object passed to it, if the object passed
+        to it doesn't provide any interfaces.  This is mostly for testing
+        convenience rather than a particularly useful feature.
+        """
+        probe = object()
+        self.assertIdentical(probe, loggedReactor(probe))
+
+
+    def test_interfaces(self):
+        """
+        The object returned by L{loggedReactor} provides all of the interfaces
+        provided by the object passed to it.
+        """
+        probe = Probe()
+        reactor = loggedReactor(probe)
+        self.assertTrue(IProbe.providedBy(reactor))
+
+
+    def test_passthrough(self):
+        """
+        Methods on interfaces on the object passed to L{loggedReactor} can be
+        called by calling them on the object returned by L{loggedReactor}.
+        """
+        expected = object()
+        probe = Probe(expected)
+        reactor = loggedReactor(probe)
+        result = reactor.probe()
+        self.assertTrue(probe._probed)
+        self.assertIdentical(expected, result)
+
+
+    def test_connectTCP(self):
+        """
+        Called on the object returned by L{loggedReactor}, C{connectTCP} calls
+        the wrapped reactor's C{connectTCP} method with the original factory
+        wrapped in a L{_TrafficLoggingFactory}.
+        """
+        class RecordDataProtocol(Protocol):
+            def dataReceived(self, data):
+                self.data = data
+        proto = RecordDataProtocol()
+        factory = ClientFactory()
+        factory.protocol = lambda: proto
+        reactor = MemoryReactor()
+        logged = loggedReactor(reactor)
+        logged.connectTCP('192.168.1.2', 1234, factory, 21, '127.0.0.2')
+        [(host, port, factory, timeout, bindAddress)] = reactor.tcpClients
+        self.assertEqual('192.168.1.2', host)
+        self.assertEqual(1234, port)
+        self.assertIsInstance(factory, _TrafficLoggingFactory)
+        self.assertEqual(21, timeout)
+        self.assertEqual('127.0.0.2', bindAddress)
+
+        # Verify that the factory and protocol specified are really being used
+        protocol = factory.buildProtocol(None)
+        protocol.makeConnection(None)
+        protocol.dataReceived("foo")
+        self.assertEqual(proto.data, "foo")
+
+
+    def test_getLogFiles(self):
+        """
+        The reactor returned by L{loggedReactor} has a C{getLogFiles} method
+        which returns a L{logstate} instance containing the active and
+        completed log files tracked by the logging wrapper.
+        """
+        wrapped = ClientFactory()
+        wrapped.protocol = Discard
+        reactor = MemoryReactor()
+        logged = loggedReactor(reactor)
+        logged.connectTCP('127.0.0.1', 1234, wrapped)
+        factory = reactor.tcpClients[0][2]
+
+        finished = factory.buildProtocol(None)
+        finished.makeConnection(StringTransport())
+        finished.dataReceived('finished')
+        finished.connectionLost(None)
+
+        active = factory.buildProtocol(None)
+        active.makeConnection(StringTransport())
+        active.dataReceived('active')
+
+        logs = logged.getLogFiles()
+        self.assertEqual(1, len(logs.finished))
+        self.assertIn('finished', logs.finished[0].getvalue())
+        self.assertEqual(1, len(logs.active))
+        self.assertIn('active', logs.active[0].getvalue())
+
+
+
+class TrafficLoggingFactoryTests(TestCase):
+    """
+    Tests for L{_TrafficLoggingFactory}.
+    """
+    def setUp(self):
+        self.wrapped = ClientFactory()
+        self.wrapped.protocol = Discard
+        self.factory = _TrafficLoggingFactory(self.wrapped)
+
+
+    def test_receivedBytesLogged(self):
+        """
+        When bytes are delivered through a protocol created by
+        L{_TrafficLoggingFactory}, they are added to a log kept on that
+        factory.
+        """
+        protocol = self.factory.buildProtocol(None)
+
+        # The factory should now have a new StringIO log file
+        self.assertEqual(1, len(self.factory.logs))
+
+        transport = StringTransport()
+        protocol.makeConnection(transport)
+
+        protocol.dataReceived("hello, world")
+        self.assertEqual(
+            "*\nC 0: 'hello, world'\n", self.factory.logs[0].getvalue())
+
+
+    def test_finishedLogs(self):
+        """
+        When connections are lost, the corresponding log files are moved into
+        C{_TrafficLoggingFactory.finishedLogs}.
+        """
+        protocol = self.factory.buildProtocol(None)
+        transport = StringTransport()
+        protocol.makeConnection(transport)
+        logfile = self.factory.logs[0]
+        protocol.connectionLost(None)
+        self.assertEqual(0, len(self.factory.logs))
+        self.assertEqual([logfile], self.factory.finishedLogs)
+
+
+    def test_finishedLogsLimit(self):
+        """
+        Only the most recent C{_TrafficLoggingFactory.LOGFILE_LIMIT} logfiles
+        are kept in C{_TrafficLoggingFactory.finishedLogs}.
+        """
+        self.factory.LOGFILE_LIMIT = 2
+        first = self.factory.buildProtocol(None)
+        first.makeConnection(StringTransport())
+        second = self.factory.buildProtocol(None)
+        second.makeConnection(StringTransport())
+        third = self.factory.buildProtocol(None)
+        third.makeConnection(StringTransport())
+
+        second.connectionLost(None)
+        first.connectionLost(None)
+        third.connectionLost(None)
+
+        self.assertEqual(
+            [first.logfile, third.logfile], self.factory.finishedLogs)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_webadmin.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_webadmin.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/tests/test_webadmin.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,144 @@
+##
+# Copyright (c) 2012-2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+from twisted.trial.unittest import TestCase
+from contrib.performance.loadtest.webadmin import LoadSimAdminResource
+
+class WebAdminTests(TestCase):
+    """
+    Tests for L{LoadSimAdminResource}.
+    """
+
+    class FakeReporter(object):
+
+        def generateReport(self, output):
+            output.write("FakeReporter")
+
+
+    class FakeReactor(object):
+
+        def __init__(self):
+            self.running = True
+
+        def stop(self):
+            self.running = False
+
+
+    class FakeLoadSim(object):
+
+        def __init__(self):
+            self.reactor = WebAdminTests.FakeReactor()
+            self.reporter = WebAdminTests.FakeReporter()
+            self.running = True
+
+        def stop(self):
+            self.running = False
+
+
+    class FakeRequest(object):
+
+        def __init__(self, **kwargs):
+            self.args = kwargs
+
+
+    def test_resourceGET(self):
+        """
+        Test render_GET
+        """
+
+        loadsim = WebAdminTests.FakeLoadSim()
+        resource = LoadSimAdminResource(loadsim)
+
+        response = resource.render_GET(WebAdminTests.FakeRequest())
+        self.assertTrue(response.startswith("<html>"))
+        self.assertTrue(response.find(resource.token) != -1)
+
+
+    def test_resourcePOST_Stop(self):
+        """
+        Test render_POST when Stop button is clicked
+        """
+
+        loadsim = WebAdminTests.FakeLoadSim()
+        resource = LoadSimAdminResource(loadsim)
+        self.assertTrue(loadsim.reactor.running)
+
+        response = resource.render_POST(WebAdminTests.FakeRequest(
+            token=(resource.token,),
+            stop=None,
+        ))
+        self.assertTrue(response.startswith("<html>"))
+        self.assertTrue(response.find(resource.token) == -1)
+        self.assertTrue(response.find("FakeReporter") != -1)
+        self.assertFalse(loadsim.running)
+
+
+    def test_resourcePOST_Stop_BadToken(self):
+        """
+        Test render_POST when Stop button is clicked but token is wrong
+        """
+
+        loadsim = WebAdminTests.FakeLoadSim()
+        resource = LoadSimAdminResource(loadsim)
+        self.assertTrue(loadsim.reactor.running)
+
+        response = resource.render_POST(WebAdminTests.FakeRequest(
+            token=("xyz",),
+            stop=None,
+        ))
+        self.assertTrue(response.startswith("<html>"))
+        self.assertTrue(response.find(resource.token) != -1)
+        self.assertTrue(response.find("FakeReporter") == -1)
+        self.assertTrue(loadsim.running)
+
+
+    def test_resourcePOST_Results(self):
+        """
+        Test render_POST when Results button is clicked
+        """
+
+        loadsim = WebAdminTests.FakeLoadSim()
+        resource = LoadSimAdminResource(loadsim)
+        self.assertTrue(loadsim.reactor.running)
+
+        response = resource.render_POST(WebAdminTests.FakeRequest(
+            token=(resource.token,),
+            results=None,
+        ))
+        self.assertTrue(response.startswith("<html>"))
+        self.assertTrue(response.find(resource.token) != -1)
+        self.assertTrue(response.find("FakeReporter") != -1)
+        self.assertTrue(loadsim.running)
+
+
+    def test_resourcePOST_Results_BadToken(self):
+        """
+        Test render_POST when Results button is clicked and token is wrong
+        """
+
+        loadsim = WebAdminTests.FakeLoadSim()
+        resource = LoadSimAdminResource(loadsim)
+        self.assertTrue(loadsim.reactor.running)
+
+        response = resource.render_POST(WebAdminTests.FakeRequest(
+            token=("xyz",),
+            results=None,
+        ))
+        self.assertTrue(response.startswith("<html>"))
+        self.assertTrue(response.find(resource.token) != -1)
+        self.assertTrue(response.find("FakeReporter") == -1)
+        self.assertTrue(loadsim.running)

Modified: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/webadmin.py
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/webadmin.py	2015-09-10 20:09:06 UTC (rev 15119)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/webadmin.py	2015-09-10 20:25:07 UTC (rev 15120)
@@ -39,6 +39,7 @@
     HEAD = """\
 <html>
 <head>
+<meta http-equiv="refresh" content="1" >
 <style type="text/css">
 body {color:#000000;}
 h1 h2 h3 {color:#333333;}
@@ -80,9 +81,11 @@
     def __init__(self, loadsim):
         self.loadsim = loadsim
         self.token = str(uuid.uuid4())
+        self.count = 0
 
 
     def render_GET(self, request):
+        self.count += 1
         return self._renderReport()
 
 
@@ -109,5 +112,5 @@
             html = self.HEAD + self.BODY_RESULTS_STOPPED
             return html % (None, report.getvalue(), ms)
         else:
-            html = self.HEAD + self.BODY_RESULTS
+            html = self.HEAD + "<h3>Request #" + str(self.count) + "</h3>" + self.BODY_RESULTS
             return html % (self.token, report.getvalue(), ms)

Added: CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/www/layout.html
===================================================================
--- CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/www/layout.html	                        (rev 0)
+++ CalendarServer/branches/users/sagen/clientsim/contrib/performance/loadtest/www/layout.html	2015-09-10 20:25:07 UTC (rev 15120)
@@ -0,0 +1,28 @@
+<html>
+<head>
+    <style type="text/css">
+        body {color:#000000;}
+        h1 h2 h3 {color:#333333;}
+        td {text-align: center; padding: 5px;}
+        pre.light {color:#CCCCCC; font-size:12px;}
+        table.rounded-corners {
+            border: 1px solid #000000; background-color:#cccccc;
+            -moz-border-radius: 5px;
+            -webkit-border-radius: 5px;
+            -khtml-border-radius: 5px;
+            border-radius: 5px;
+        }
+    </style>
+</head>
+<body>
+    <h1>Load Simulator Web Admin</h1>
+    <form method="POST">
+        <input name="token" type="hidden" value="%s" />
+        <table class="rounded-corners">
+            <tr><td><input name="results" type="submit" value="Refresh" /></td></tr>
+            <tr><td><input name="stop" type="submit" value="Stop Sim" /></td></tr>
+        </table>
+    </form>
+    {% block results %}
+</body>
+</html>
\ No newline at end of file
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150910/9ae209bd/attachment-0001.html>


More information about the calendarserver-changes mailing list