[CalendarServer-changes] [15048] CalendarServer/branches/users/sredmond/clientsim/contrib/ performance/loadtest

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 17 14:14:42 PDT 2015


Revision: 15048
          http://trac.calendarserver.org//changeset/15048
Author:   sredmond at apple.com
Date:     2015-08-17 14:14:42 -0700 (Mon, 17 Aug 2015)
Log Message:
-----------
Cleans up old files from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/request-data/OS_X_10_7/user_list_principal_property_search.request

Added Paths:
-----------
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/calendars-only.plist
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/clients-old.plist
    CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py

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

Deleted: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/clients.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/clients.plist	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/clients.plist	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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>

Deleted: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.dist.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.dist.plist	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.dist.plist	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/config.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.plist	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.plist	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/distributions.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py	                        (rev 0)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -0,0 +1,432 @@
+##
+# 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:
+Base statistical functions
+  mean / median / stddev / mad / residuals
+
+IPopulation interface exposes:
+  sample()
+
+Sampling from this distribution must *not* change the underlying behavior of a distribution
+
+Distributions (all of which implement IPopulation):
+  UniformDiscreteDistribution
+  LogNormalDistribution
+  FixedDistribution
+  NearFutureDistribution
+  NormalDistribution
+  UniformIntegerDistribution
+  WorkDistribution
+  RecurrenceDistribution
+"""
+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 IPopulation(Interface):
+    """Interface for a class that provides a single function, `sample`, which returns a float"""
+    def sample(): #@NoSelf
+        pass
+
+
+class UniformDiscreteDistribution(object, FancyEqMixin):
+    """
+
+    """
+    implements(IPopulation)
+
+    compareAttributes = ['_values']
+
+    def __init__(self, values, randomize=True):
+        self._values = values
+        self._randomize = randomize
+        self._refill()
+
+
+    def _refill(self):
+        self._remaining = self._values[:]
+        if self._randomize:
+            random.shuffle(self._remaining)
+
+
+    def sample(self):
+        if not self._remaining:
+            self._refill()
+        return self._remaining.pop()
+
+
+
+class LogNormalDistribution(object, FancyEqMixin):
+    """
+    """
+    implements(IPopulation)
+
+    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(IPopulation)
+
+    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 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
+
+class HappyFaceStartDistribution(object, FancyEqMixin):
+    compareAttributes = []
+    def __init__(self, scale):
+        self._scale = scale
+
+    def sample(self):
+        return nprandom.exponential(self._scale)
+
+class HappyFaceDurationDistribution(object, FancyEqMixin):
+    compareAttributes = []
+    def __init__(self):
+        self._distribution = UniformDiscreteDistribution(60 * 15, 60 * 30)
+
+    def sample(self):
+        return self._distribution.sample()
+
+
+if __name__ == '__main__':
+    from collections import defaultdict
+    mu = 1.5
+    sigma = 1.22
+    distribution = LogNormalDistribution(mu, sigma, 100)
+    result = defaultdict(int)
+    for i in range(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/sredmond/clientsim/contrib/performance/loadtest/request-data/OS_X_10_7/user_list_principal_property_search.request
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/request-data/OS_X_10_7/user_list_principal_property_search.request	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/request-data/OS_X_10_7/user_list_principal_property_search.request	2015-08-17 21:14:42 UTC (rev 15048)
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="utf-8" ?>
-<x0:principal-property-search xmlns:x2="urn:ietf:params:xml:ns:caldav" xmlns:x0="DAV:" xmlns:x1="http://calendarserver.org/ns/" test="anyof"><x0:property-search><x0:prop><x0:displayname/></x0:prop><x0:match match-type="starts-with">%(displayname)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:email-address-set/></x0:prop><x0:match match-type="starts-with">%(email)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:first-name/></x0:prop><x0:match match-type="starts-with">%(firstname)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:last-name/></x0:prop><x0:match match-type="starts-with">%(lastname)s</x0:match></x0:property-search><x0:prop><x1:email-address-set/><x2:calendar-user-address-set/><x2:calendar-user-type/><x0:displayname/><x1:last-name/><x1:first-name/><x1:record-type/><x0:principal-URL/></x0:prop></x0:principal-property-search>
\ No newline at end of file
+<x0:principal-property-search xmlns:x2="urn:ietf:params:xml:ns:caldav" xmlns:x0="DAV:" xmlns:x1="http://calendarserver.org/ns/" test="anyof"><x0:property-search><x0:prop><x0:displayname/></x0:prop><x0:match match-type="starts-with">%(displayname)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:email-address-set/></x0:prop><x0:match match-type="starts-with">%(email)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:first-name/></x0:prop><x0:match match-type="starts-with">%(firstname)s</x0:match></x0:property-search><x0:property-search><x0:prop><x1:last-name/></x0:prop><x0:match match-type="starts-with">%(lastname)s</x0:match></x0:property-search><x0:prop><x1:email-address-set/><x2:calendar-user-address-set/><x2:calendar-user-type/><x0:displayname/><x1:last-name/><x1:first-name/><x1:record-type/><x0:principal-URL/></x0:prop></x0:principal-property-search>

Added: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/calendars-only.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/calendars-only.plist	                        (rev 0)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/calendars-only.plist	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/standard-configs/clients-old.plist
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/clients-old.plist	                        (rev 0)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/standard-configs/clients-old.plist	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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.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>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.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>
+						</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>

Deleted: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_ical.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_ical.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_ical.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/test_population.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_population.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_population.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/test_profiles.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_profiles.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_profiles.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/test_sim.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_sim.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_sim.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/test_trafficlogger.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_trafficlogger.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_trafficlogger.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/test_webadmin.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_webadmin.py	2015-08-17 20:38:08 UTC (rev 15047)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/test_webadmin.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -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/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py
===================================================================
--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py	                        (rev 0)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py	2015-08-17 21:14:42 UTC (rev 15048)
@@ -0,0 +1,101 @@
+from twisted.trial.unittest import TestCase
+
+from contrib.performance.loadtest.distributions import (
+    LogNormalDistribution, UniformDiscreteDistribution,
+    UniformIntegerDistribution, WorkDistribution, RecurrenceDistribution
+)
+
+from pycalendar.datetime import DateTime
+from pycalendar.timezone import Timezone
+
+class DistributionTests(TestCase):
+    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)
+
+
+    def test_uniformdiscrete(self):
+        population = [1, 5, 6, 9]
+        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)
+
+
+    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], randomize=False)
+        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)
+
+
+    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)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150817/b0b5fe2b/attachment-0001.html>


More information about the calendarserver-changes mailing list