<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[15081] CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/15081">15081</a></dd>
<dt>Author</dt> <dd>sredmond@apple.com</dd>
<dt>Date</dt> <dd>2015-08-31 13:37:45 -0700 (Mon, 31 Aug 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Adds Python configuration and a lot more</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestampsimpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestconfigpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestdistributionspy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtesticalpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestloggerpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestpopulationpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestprofilespy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestpushpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestrecordspy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestrequesterpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestresourcespy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsaltsettingsplistclientsplist">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsclientspy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsconfigpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsimpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_distributionspy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_pushpy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py</a></li>
<li><a href="#CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_resourcespy">CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestampsimpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ampsim.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -46,7 +46,6 @@
</span><span class="cx">             exit(0)
</span><span class="cx">     runmain()
</span><span class="cx"> 
</span><del>-
</del><span class="cx"> from copy import deepcopy
</span><span class="cx"> 
</span><span class="cx"> from plistlib import writePlistToString, readPlistFromString
</span><span class="lines">@@ -58,14 +57,16 @@
</span><span class="cx"> 
</span><span class="cx"> from contrib.performance.loadtest.sim import LoadSimulator
</span><span class="cx"> from contrib.performance.loadtest.records import DirectoryRecord
</span><ins>+from contrib.performance.loadtest.config import Config
</ins><span class="cx"> 
</span><span class="cx"> class Configure(Command):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Configure this worker process with the text of an XML property list.
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    arguments = [(&quot;plist&quot;, String())]
</del><ins>+    arguments = [(&quot;cfg&quot;, Pickle())]
</ins><span class="cx">     # Pass OSError exceptions through, presenting the exception message to the user.
</span><del>-    errors = {OSError: 'OSError'}
</del><ins>+    # errors = {OSError: 'OSError'}
+    errors = {Exception: 'Exception'}
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -112,11 +113,16 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @Configure.responder
</span><del>-    def config(self, plist):
</del><ins>+    def config(self, cfg):
</ins><span class="cx">         from sys import stderr
</span><del>-        cfg = readPlistFromString(plist)
</del><ins>+        # cfg = readPlistFromString(plist)
+        config = Config.deserializeFromWorker(cfg)
+        with open('logs.txt', 'a') as f:
+            f.write('here')
+            f.write(str(config.__dict__))
+
</ins><span class="cx">         addObserver(self.emit)
</span><del>-        sim = LoadSimulator.fromConfig(cfg)
</del><ins>+        sim = LoadSimulator.fromConfigObject(config)
</ins><span class="cx">         sim.records = self.records
</span><span class="cx">         sim.attachServices(stderr)
</span><span class="cx">         return {}
</span><span class="lines">@@ -161,26 +167,29 @@
</span><span class="cx">                             email=record.email,
</span><span class="cx">                             guid=record.guid)
</span><span class="cx"> 
</span><del>-        workerConfig = deepcopy(self.loadsim.configTemplate)
-        # The list of workers is for the manager only; the workers themselves
-        # know they're workers because they _don't_ receive this list.
-        del workerConfig[&quot;workers&quot;]
-        # The manager loads the accounts via the configured loader, then sends
-        # them out to the workers (right above), which look at the state at an
-        # instance level and therefore don't need a globally-named directory
-        # record loader.
-        del workerConfig[&quot;accounts&quot;]
</del><ins>+        workerConfig = self.loadsim.configTemplate(self.whichWorker, self.numWorkers)
+        dupe = deepcopy(workerConfig)
+        del dupe['records']
+        print dupe
+        # # The list of workers is for the manager only; the workers themselves
+        # # know they're workers because they _don't_ receive this list.
+        # del workerConfig[&quot;workers&quot;]
+        # # The manager loads the accounts via the configured loader, then sends
+        # # them out to the workers (right above), which look at the state at an
+        # # instance level and therefore don't need a globally-named directory
+        # # record loader.
+        # del workerConfig[&quot;accounts&quot;]
</ins><span class="cx"> 
</span><del>-        workerConfig[&quot;workerID&quot;] = self.whichWorker
-        workerConfig[&quot;workerCount&quot;] = self.numWorkers
-        workerConfig[&quot;observers&quot;] = []
-        workerConfig.pop(&quot;accounts&quot;, None)
</del><ins>+        # workerConfig[&quot;workerID&quot;] = self.whichWorker
+        # workerConfig[&quot;workerCount&quot;] = self.numWorkers
+        # workerConfig[&quot;observers&quot;] = []
+        # workerConfig.pop(&quot;accounts&quot;, None)
</ins><span class="cx"> 
</span><del>-        plist = writePlistToString(workerConfig)
</del><ins>+        # plist = writePlistToString(workerConfig)
</ins><span class="cx">         self.output.write(&quot;Initiating worker configuration\n&quot;)
</span><span class="cx">         def completed(x):
</span><span class="cx">             self.output.write(&quot;Worker configuration complete.\n&quot;)
</span><del>-        self.callRemote(Configure, plist=plist).addCallback(completed)
</del><ins>+        self.callRemote(Configure, cfg=workerConfig).addCallback(completed)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @LogMessage.responder
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/config.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,69 +1,127 @@
</span><span class="cx"> from importlib import import_module
</span><span class="cx"> 
</span><ins>+from twisted.python.log import msg
</ins><span class="cx"> from contrib.performance.loadtest.logger import ReportStatistics, RequestLogger, OperationLogger
</span><del>-from contrib.performance.loadtest.sim import recordsFromCSVFile
</del><ins>+from contrib.performance.loadtest.records import recordsFromCSVFile
+from contrib.performance.loadtest.population import ClientFactory, PopulationParameters
</ins><span class="cx"> 
</span><del>-DEFAULTS = {
-    server = &quot;https://127.0.0.1:8443&quot;
</del><ins>+class Config(object):
+    def __init__(self):
+        pass
</ins><span class="cx"> 
</span><del>-    accounts = recordsFromCSVFile(&quot;contrib/performance/loadtest/accounts.csv&quot;)
</del><ins>+    def populateFrom(self, serverConfig, clientConfig, usePlist=False):
+        # If there is a list of workers, then this process is *not* a worker
+        isManager = serverConfig.get('workers') is not None
</ins><span class="cx"> 
</span><del>-    _requestLogger = RequestLogger()
-    _operationLogger = OperationLogger(
-        thresholdsPath=&quot;contrib/performance/loadtest/thresholds.json&quot;,
-        lagCutoff=1.0,
-        failCutoff=1.0
-    )
-    _statisticsReporter = ReportStatistics(
-        thresholdsPath=&quot;contrib/performance/loadtest/thresholds.json&quot;,
-        benchmarksPath=&quot;contrib/performance/loadtest/benchmarks.json&quot;,
-        failCutoff=1.0
-    )
</del><ins>+        if usePlist:
+            # If the supplied files are plists, we need to convert the named objects into real Python objects.
+            # The ensuing hacky code is why I recommend we remove support for plist-based configuration
+            workers = config['workers']
+            if not isManager:
+                # Client / place where the simulator actually runs configuration
+                workerID = config.get(&quot;workerID&quot;, 0)
+                workerCount = config.get(&quot;workerCount&quot;, 1)
+                configTemplate = None
+                server = config.get('server', 'http://127.0.0.1:8008')
+                serializationPath = None
</ins><span class="cx"> 
</span><del>-    arrival = SmoothRampUp(
-        groups=2,
-        groupSize=1,
-        interval=3,
-        clientsPerUser=1
-    )
-}
</del><ins>+                serializationPath = config['serializationPath']
</ins><span class="cx"> 
</span><del>-class Config(object):
</del><ins>+                if 'arrival' in config:
+                    arrival = Arrival(
+                        namedAny(config['arrival']['factory']),
+                        config['arrival']['params'])
+                else:
+                    arrival = Arrival(
+                        SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
</ins><span class="cx"> 
</span><ins>+                parameters = PopulationParameters()
+                if 'clients' in config:
+                    for clientConfig in config['clients']:
+                        parameters.addClient(
+                            clientConfig[&quot;weight&quot;],
+                            ClientType(
+                                clientConfig[&quot;software&quot;],
+                                clientConfig[&quot;params&quot;],
+                                clientConfig[&quot;profiles&quot;]
+                            )
+                        )
+                            # ClientType(
+                            #     namedAny(clientConfig[&quot;software&quot;]),
+                            #     cls._convertParams(clientConfig[&quot;params&quot;]),
+                            #     [
+                            #         ProfileType(
+                            #             namedAny(profile[&quot;class&quot;]),
+                            #             cls._convertParams(profile[&quot;params&quot;])
+                            #         ) for profile in clientConfig[&quot;profiles&quot;]
+                            #     ]))
+                if not parameters.clients:
+                    parameters.addClient(1,
+                                         ClientType(OS_X_10_6, {},
+                                                    [Eventer, Inviter, Accepter]))
+            else:
+                # Manager / observer process.
+                server = ''
+                serializationPath = None
+                arrival = None
+                parameters = None
+                workerID = 0
+                configTemplate = config
+                workerCount = 1
</ins><span class="cx"> 
</span><del>-    def __init__(self, serverConfigFile, clientConfigFile):
-        # These are modules
-        serverConfigModule = import_module(serverConfigFile)
-        clientConfigModule = import_module(clientConfigFile)
</del><ins>+            # webadminPort = 
+            webadminPort = None
+            if 'webadmin' in config:
+                if config['webadmin']['enabled']:
+                    webadminPort = config['webadmin']['HTTPPort']
</ins><span class="cx"> 
</span><del>-        self.clients = clientConfigModule.clientConfiguration
-        self.workers = workers
-        self.configTemplate = configTemplate
-        self.workerID = workerID
-        self.workerCount = workerCount
</del><ins>+            serverStats = None
+            if 'serverStats' in config:
+                if config['serverStats']['enabled']:
+                    serverStats = config['serverStats']
+                    serverStats['server'] = config['server'] if 'server' in config else ''
</ins><span class="cx"> 
</span><del>-        self.server = serverConfig.get('server')
-        self.webadminPort = serverConfig.get('webadminPort')
-        self.serverStats = serverConfig.get('serverStatsPort')
-        self.serializationPath = serverConfig.get('serializationPath')
-        self.arrival = serverConfig.get('arrival')
-        self.observers = serverConfig.get('observers')
-        self.records = serverConfig.get('records')
-        self.workers = serverConfig.get('workers')
</del><ins>+            observers = []
+            if 'observers' in config:
+                for observer in config['observers']:
+                    observerName = observer[&quot;type&quot;]
+                    observerParams = observer[&quot;params&quot;]
+                    observers.append(namedAny(observerName)(**observerParams))
</ins><span class="cx"> 
</span><del>-        self.buildParameters()
</del><ins>+            records = []
+            if 'accounts' in config:
+                loader = config['accounts']['loader']
+                params = config['accounts']['params']
+                records.extend(namedAny(loader)(**params))
+                output.write(&quot;Loaded {0} accounts.\n&quot;.format(len(records)))
</ins><span class="cx"> 
</span><del>-    def buildParameters(self):
-        self.parameters = PopulationParameters()
-        for client in self.clients:
-            self.parameters.addClient(
</del><ins>+        else:
+            # Python configuration - super easy! Look! It's great!
+                self.webadminPort = serverConfig.get('webadminPort')
+                self.serverStats = serverConfig.get('serverStatsPort')
+                self.observers = serverConfig.get('observers') # Workers shouldn't need this
+                self.workers = serverConfig.get('workers')
+
+                self.server = serverConfig.get('server')
+                self.serializationPath = serverConfig.get('serializationPath')
+                self.arrival = serverConfig.get('arrival')
+                self.records = serverConfig.get('records')
+                self.workerID = serverConfig.get('workerID', 0)
+                self.workerCount = serverConfig.get('workerCount', 1)
+                self.parameters = self.buildParameters(clientConfig)
+
+    def buildParameters(self, clients):
+        parameters = PopulationParameters()
+        for client in clients:
+            parameters.addClient(
</ins><span class="cx">                 client[&quot;weight&quot;],
</span><del>-                ClientType(
</del><ins>+                ClientFactory(
</ins><span class="cx">                     client[&quot;software&quot;],
</span><span class="cx">                     client[&quot;params&quot;],
</span><span class="cx">                     client[&quot;profiles&quot;]
</span><span class="cx">                 )
</span><span class="cx">             )
</span><ins>+        return parameters
</ins><span class="cx"> 
</span><span class="cx">     def buildSerializationPath(self):
</span><span class="cx">         if self.serializationPath:
</span><span class="lines">@@ -75,7 +133,54 @@
</span><span class="cx">                     print(&quot;Please consult the clientDataSerialization stanza of contrib/performance/loadtest/config.plist&quot;)
</span><span class="cx">                     raise
</span><span class="cx"> 
</span><del>-    def get(self, attr):
-        if hasattr(self, attr):
-            return getattr(self, attr)
-        return DEFAULTS.get(attr, None)
</del><ins>+    def serializeForWorker(self, workerID, workerCount):
+        if not self.workers: # If we are workers, don't try to be a manager
+            return {}
+        # print &quot;Trying to serialize for worker #&quot; + str(workerID)
+        # print &quot;My info, btw is &quot; + str(self.__dict__)
+        info = {
+            'webadminPort': '',
+            'serverStats': '',
+            'workers': [],
+            'observers': [],
+            'workerID': workerID,
+            'workerCount': workerCount,
+            # Workers need some information to work correctly
+            'server': self.server,
+            'serializationPath': self.serializationPath,
+            'arrival': self.arrival,
+            'records': self.records,
+            'parameters': self.parameters
+        }
+        return info
+
+    @classmethod
+    def deserializeFromWorker(cls, info):
+        base = cls()
+        base.__dict__.update(info)
+        return base
+
+    # Goodness, how awkward is this code? If we dropped support for plists, we could do away with it
+    @classmethod
+    def _convertParams(cls, params):
+        &quot;&quot;&quot;
+        Find parameter values which should be more structured than plistlib is
+        capable of constructing and replace them with the more structured form.
+
+        Specifically, find keys that end with C{&quot;Distribution&quot;} and convert
+        them into some kind of distribution object using the associated
+        dictionary of keyword arguments.
+        &quot;&quot;&quot;
+        for k, v in params.iteritems():
+            if k.endswith('Distribution'): # Goodness how fragile
+                params[k] = cls._convertDistribution(v)
+        return params
+
+
+    @classmethod
+    def _convertDistribution(cls, value):
+        &quot;&quot;&quot;
+        Construct and return a new distribution object using the type and
+        params specified by C{value}.
+        &quot;&quot;&quot;
+        return namedAny(value['type'])(**value['params'])
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestdistributionspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/distributions.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -26,14 +26,36 @@
</span><span class="cx"> Sampling from this distribution must *not* change the underlying behavior of a distribution
</span><span class="cx"> 
</span><span class="cx"> Distributions (all of which implement IDistribution):
</span><del>-  UniformDiscreteDistribution
-  LogNormalDistribution
-  FixedDistribution
-  NearFutureDistribution
-  NormalDistribution
-  UniformIntegerDistribution
-  WorkDistribution
-  RecurrenceDistribution
</del><ins>+  # Discrete Distributions / Finite Support
+  Bernoulli
+  Binomial
+  Rademacher
+  Fixed
+  UniformDiscrete
+  UniformInteger
+
+  # Discrete Distributions / Infinite Support (&gt; 0)
+  Poisson
+  Geometric
+
+  LogNormal
+
+  Normal
+  UniformReal
+  Triangular
+  Beta
+  ChiSquared
+  Exponential
+  Gamma
+
+  # CalendarServer Specific
+  NearFuture
+  Work
+  Recurrence
+
+# TODO
+Implement simple ones through large ones
+Squeeze / pinch
</ins><span class="cx"> &quot;&quot;&quot;
</span><span class="cx"> from math import log, sqrt
</span><span class="cx"> from time import mktime
</span><span class="lines">@@ -54,9 +76,7 @@
</span><span class="cx">     def sample(): #@NoSelf
</span><span class="cx">         pass
</span><span class="cx"> 
</span><del>-# class Bounded
</del><span class="cx"> 
</span><del>-
</del><span class="cx"> class UniformDiscreteDistribution(object, FancyEqMixin):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="lines">@@ -65,22 +85,11 @@
</span><span class="cx"> 
</span><span class="cx">     compareAttributes = ['_values']
</span><span class="cx"> 
</span><del>-    def __init__(self, values, randomize=True):
</del><ins>+    def __init__(self, values):
</ins><span class="cx">         self._values = values
</span><del>-        self._randomize = randomize
-        self._refill()
</del><span class="cx"> 
</span><del>-
-    def _refill(self):
-        self._remaining = self._values[:]
-        if self._randomize:
-            random.shuffle(self._remaining)
-
-
</del><span class="cx">     def sample(self):
</span><del>-        if not self._remaining:
-            self._refill()
-        return self._remaining.pop()
</del><ins>+        return random.choice(self._values)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -218,7 +227,20 @@
</span><span class="cx">         return 1 if random.random() &lt;= self._p else 0
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class RademacherDistribution(object, FancyEqMixin):
+    &quot;&quot;&quot;
+    Takes value 1 with probability 1/2 and value -1 with probability 1/2
+    &quot;&quot;&quot;
+    def __init__(self):
+        &quot;&quot;&quot;
+        &quot;&quot;&quot;
+        self._d = BernoulliDistribution(proportion=0.5)
</ins><span class="cx"> 
</span><ins>+    def sample(self):
+        return [-1, 1][self._d.sample()]
+
+
+
</ins><span class="cx"> class BinomialDistribution(object, FancyEqMixin):
</span><span class="cx">     compareAttributes = [&quot;_successProbability&quot;, &quot;_numTrials&quot;]
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtesticalpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/ical.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -249,7 +249,6 @@
</span><span class="cx">         ampPushPort=62311,
</span><span class="cx">     ):
</span><span class="cx">         self._client_id = str(uuid4())
</span><del>-
</del><span class="cx">         self.reactor = reactor
</span><span class="cx"> 
</span><span class="cx">         self.requester = Requester(
</span><span class="lines">@@ -432,6 +431,7 @@
</span><span class="cx">             self.checkCalendarsForEvents, calendarHome)
</span><span class="cx">         return pollCalendarHome.start(self.calendarHomePollInterval, now=False)
</span><span class="cx"> 
</span><ins>+    ### TODO this doesn't seem to always work
</ins><span class="cx">     @inlineCallbacks
</span><span class="cx">     def updateCalendarHomeFromPush(self, calendarHomeSet):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="lines">@@ -538,8 +538,8 @@
</span><span class="cx">                 except KeyError:
</span><span class="cx">                     pass
</span><span class="cx">                 else:
</span><del>-                    if pushkey:
-                        self.monitor.addPushkey(href, pushkey)
</del><ins>+                    if pushkey and self.monitor:
+                        self.monitor.addPushkey(pushkey, href)
</ins><span class="cx"> 
</span><span class="cx">             nodes = results[href].getNodeProperties()
</span><span class="cx">             for nodeType in nodes[davxml.resourcetype]:
</span><span class="lines">@@ -888,7 +888,7 @@
</span><span class="cx"> 
</span><span class="cx">         # Start monitoring AMP push notifications, if possible
</span><span class="cx">         if self.monitor and self.monitor.isSubscribedTo(calendarHome):
</span><del>-            self.monitor.begin()
</del><ins>+            yield self.monitor.begin()
</ins><span class="cx">             # Run indefinitely.
</span><span class="cx">             yield Deferred()
</span><span class="cx">         else:
</span><span class="lines">@@ -902,6 +902,8 @@
</span><span class="cx">         Called before connections are closed, giving a chance to clean up
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         self.serialize()
</span><ins>+        if not self.monitor:
+            return succeed(None)
</ins><span class="cx">         return self.monitor.end()
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestloggerpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/logger.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -107,7 +107,23 @@
</span><span class="cx">             self._printRow(output, formats, self._summarizeData(method, data))
</span><span class="cx"> 
</span><span class="cx"> 
</span><ins>+class MessageLogger(object):
+    def observe(self, event):
+        if event.get(&quot;type&quot;) == &quot;log&quot;:
+            import random
+            identifier = random.random()
+            print(TerminalColors.WARNING + str(identifier) + '/' + event.get('val') + ':' + event.get('text') + TerminalColors.ENDC)
</ins><span class="cx"> 
</span><ins>+
+class EverythingLogger(object):
+    def observe(self, event):
+        # if event.get(&quot;type&quot;) == &quot;response&quot;:
+        #     from pprint import pprint
+        #     pprint(event)
+        pass
+
+
+
</ins><span class="cx"> class RequestLogger(object):
</span><span class="cx">     format = u&quot;%(user)s request %(code)s%(success)s[%(duration)5.2f s] %(method)8s %(url)s&quot;
</span><span class="cx">     success = u&quot;\N{CHECK MARK}&quot;
</span><span class="lines">@@ -129,7 +145,7 @@
</span><span class="cx">             else:
</span><span class="cx">                 formatArgs['success'] = self.failure
</span><span class="cx">                 start = TerminalColors.FAIL
</span><del>-            print(start + (self.format % formatArgs).encode('utf-8') + TerminalColors.ENDC)
</del><ins>+            print(start + (self.format % formatArgs).encode('utf-8') + &quot;from Logger w/ id: &quot; + str(id(self)) + TerminalColors.ENDC)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def report(self, output):
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestpopulationpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/population.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -43,51 +43,51 @@
</span><span class="cx"> from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class ProfileType(object, FancyEqMixin):
-    &quot;&quot;&quot;
-    @ivar profileType: A L{ProfileBase} subclass, or an L{ICalendarUserProfile}
-        implementation.
</del><ins>+# class ProfileType(object, FancyEqMixin):
+#     &quot;&quot;&quot;
+#     @ivar profileType: A L{ProfileBase} subclass
+#     @type profileType: C{type}
</ins><span class="cx"> 
</span><del>-    @ivar params: A C{dict} which will be passed to C{profileType} as keyword
-        arguments to create a new profile instance.
-    &quot;&quot;&quot;
-    compareAttributes = (&quot;profileType&quot;, &quot;params&quot;)
</del><ins>+#     @ivar params: A C{dict} which will be passed to C{profileType} as keyword
+#         arguments to create a new profile instance.
+#     &quot;&quot;&quot;
+#     compareAttributes = (&quot;profileType&quot;, &quot;params&quot;)
</ins><span class="cx"> 
</span><del>-    def __init__(self, profileType, params):
-        self.profileType = profileType
-        self.params = params
</del><ins>+#     def __init__(self, profileType, params):
+#         self.profileType = profileType
+#         self.params = params
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def __call__(self, reactor, simulator, client, number):
-        base = self.profileType(**self.params)
-        base.setUp(reactor, simulator, client, number)
-        return base
</del><ins>+#     def __call__(self, reactor, simulator, client, number):
+#         base = self.profileType(**self.params)
+#         base.setUp(reactor, simulator, client, number)
+#         return base
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def __repr__(self):
-        return &quot;ProfileType(%s, params=%s)&quot; % (self.profileType.__name__, self.params)
</del><ins>+#     def __repr__(self):
+#         return &quot;ProfileType(%s, params=%s)&quot; % (self.profileType.__name__, self.params)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class ClientType(object, FancyEqMixin):
</del><ins>+class ClientFactory(object, FancyEqMixin):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    @ivar clientType: An L{ICalendarClient} implementation
</del><ins>+    @ivar clientType: An L{BaseAppleClient} subclass
+    @ivar params: A C{dict} which will be passed to C{clientType} as keyword
+        arguements to create a new client
</ins><span class="cx">     @ivar profileTypes: A list of L{ProfileType} instances
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     compareAttributes = (&quot;clientType&quot;, &quot;profileTypes&quot;)
</span><span class="cx"> 
</span><del>-    def __init__(self, clientType, clientParams, profileTypes):
</del><ins>+    def __init__(self, clientType, clientParams, profiles):
</ins><span class="cx">         self.clientType = clientType
</span><span class="cx">         self.clientParams = clientParams
</span><del>-        self.profileTypes = profileTypes
</del><ins>+        self.profiles = profiles
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def new(self, reactor, serverAddress, serializationPath, userRecord, authInfo):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Create a new instance of this client type.
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        # print(self.clientType)
-        # print(self.clientParams)
</del><span class="cx">         return self.clientType(
</span><span class="cx">             reactor, serverAddress, serializationPath,
</span><span class="cx">             userRecord, authInfo, **self.clientParams
</span><span class="lines">@@ -120,60 +120,67 @@
</span><span class="cx">         self.clients.append((weight, clientType))
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def clientTypes(self):
-        &quot;&quot;&quot;
-        Return a list of two-tuples giving the weights and types of
-        clients in the population.
-        &quot;&quot;&quot;
-        return self.clients
</del><ins>+    def clientGenerator(self):
+        while True:
+            for (weight, clientFactory) in self.clients:
+                for _ignore_i in xrange(weight):
+                    yield clientFactory
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+    # def clientTypes(self):
+    #     &quot;&quot;&quot;
+    #     Return a list of two-tuples giving the weights and types of
+    #     clients in the population.
+    #     &quot;&quot;&quot;
+    #     return self.clients
</ins><span class="cx"> 
</span><del>-class Populator(object):
-    &quot;&quot;&quot;
-    @ivar userPattern: A C{str} giving a formatting pattern to use to
-        construct usernames.  The string will be interpolated with a
-        single integer, the incrementing counter of how many users
-        have thus far been &quot;used&quot;.
</del><span class="cx"> 
</span><del>-    @ivar passwordPattern: Similar to C{userPattern}, but for
-        passwords.
-    &quot;&quot;&quot;
-    def __init__(self, random):
-        self._random = random
</del><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def _cycle(self, elements):
-        while True:
-            for (weight, value) in elements:
-                for _ignore_i in range(weight):
-                    yield value
</del><span class="cx"> 
</span><ins>+# class Populator(object):
+#     &quot;&quot;&quot;
+#     &quot;&quot;&quot;
+#     def __init__(self):
+#         self._random = random
</ins><span class="cx"> 
</span><del>-    def populate(self, parameters):
-        &quot;&quot;&quot;
-        Generate individuals such as might be randomly selected from a
-        population with the given parameters.
</del><span class="cx"> 
</span><del>-        @type parameters: L{PopulationParameters}
-        @rtype: generator of L{ClientType} instances
-        &quot;&quot;&quot;
-        for (clientType,) in izip(self._cycle(parameters.clientTypes())):
-            yield clientType
</del><ins>+#     def _cycle(self, elements):
+#         while True:
+#             for (weight, value) in elements:
+#                 for _ignore_i in range(weight):
+#                     yield value
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+#     def populate(self, parameters):
+#         &quot;&quot;&quot;
+#         Generate individuals such as might be randomly selected from a
+#         population with the given parameters.
</ins><span class="cx"> 
</span><ins>+#         @type parameters: L{PopulationParameters}
+#         @rtype: generator of L{ClientType} instances
+#         &quot;&quot;&quot;
+#         for (clientType,) in izip(self._cycle(parameters.clientTypes())):
+#             yield clientType
+
+
+
</ins><span class="cx"> class CalendarClientSimulator(object):
</span><del>-    def __init__(self, records, populator, parameters, reactor, server,
</del><ins>+    def __init__(self, records, parameters, reactor, server,
</ins><span class="cx">                  serializationPath, workerIndex=0, workerCount=1):
</span><ins>+        import random
+        # i = random.randint(0, 1000)
+        # with open('log%d.txt'.format(i), 'a') as f:
+        #     f.write('wtf')
+        val = random.random()
+        msg(type=&quot;log&quot;, text=&quot;In create client sim&quot;, val=str(val))
</ins><span class="cx">         from pprint import pprint
</span><span class="cx">         pprint(records)
</span><span class="cx">         self._records = records
</span><del>-        self.populator = populator
</del><span class="cx">         self.reactor = reactor
</span><span class="cx">         self.server = server
</span><span class="cx">         self.serializationPath = serializationPath
</span><del>-        self._pop = self.populator.populate(parameters)
</del><ins>+        self._populator = parameters.clientGenerator()
</ins><span class="cx">         self._user = 0
</span><span class="cx">         self._stopped = False
</span><span class="cx">         self.workerIndex = workerIndex
</span><span class="lines">@@ -228,63 +235,74 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def add(self, numClients, clientsPerUser):
</span><del>-        for _ignore_n in range(numClients):
</del><ins>+        # for _ignore_n in range(numClients):
+        #     number = self._nextUserNumber()
+
+        #     for _ignore_peruser in range(clientsPerUser):
+        #         clientType = self._populator.next()
+        #         if (number % self.workerCount) != self.workerIndex:
+        #             # If we're in a distributed work scenario and we are worker N,
+        #             # we have to skip all but every Nth request (since every node
+        #             # runs the same arrival policy).
+        #             continue
+
+        #         _ignore_user, auth = self._createUser(number)
+
+        #         reactor = loggedReactor(self.reactor)
+        #         client = clientType.new(
+        #             reactor,
+        #             self.server,
+        #             self.serializationPath,
+        #             self.getUserRecord(number),
+        #             auth,
+        #         )
+        #         self.clients.append(client)
+        #         d = client.run()
+        #         d.addErrback(self._clientFailure, reactor)
+
+        #         for profileType in clientType.profileTypes:
+        #             print(profileType)
+        #             profile = profileType(reactor, self, client, number)
+        #             if profile.enabled:
+        #                 d = profile.initialize()
+        #                 def _run(result):
+        #                     d2 = profile.run()
+        #                     d2.addErrback(self._profileFailure, profileType, reactor)
+        #                     return d2
+        #                 d.addCallback(_run)
+
+        for i in range(numClients):
</ins><span class="cx">             number = self._nextUserNumber()
</span><del>-
-            for _ignore_peruser in range(clientsPerUser):
-                clientType = self._pop.next()
</del><ins>+            # What user are we representing?
+            for j in range(clientsPerUser):
</ins><span class="cx">                 if (number % self.workerCount) != self.workerIndex:
</span><span class="cx">                     # If we're in a distributed work scenario and we are worker N,
</span><span class="cx">                     # we have to skip all but every Nth request (since every node
</span><span class="cx">                     # runs the same arrival policy).
</span><span class="cx">                     continue
</span><ins>+                clientFactory = self._populator.next()
</ins><span class="cx"> 
</span><span class="cx">                 _ignore_user, auth = self._createUser(number)
</span><ins>+                reactor = loggedReactor(self.reactor)
</ins><span class="cx"> 
</span><del>-                reactor = loggedReactor(self.reactor)
-                client = clientType.new(
-                    reactor,
</del><ins>+                client = clientFactory.new(
+                    self.reactor,
</ins><span class="cx">                     self.server,
</span><span class="cx">                     self.serializationPath,
</span><span class="cx">                     self.getUserRecord(number),
</span><del>-                    auth,
</del><ins>+                    auth
</ins><span class="cx">                 )
</span><ins>+                import random
</ins><span class="cx">                 self.clients.append(client)
</span><del>-                d = client.run()
-                d.addErrback(self._clientFailure, reactor)
</del><ins>+                client.run().addErrback(self._clientFailure, reactor)
+                for profileTemplate in clientFactory.profiles:
+                    print(&quot;Templated interval is:&quot; + str(profileTemplate._interval))
+                    profile = profileTemplate.duplicate()
+                    print(&quot;I see the interval as:&quot; + str(profile._interval))
+                    profile.setUp(self.reactor, self, client, number)
+                    profile.run().addErrback(self._profileFailure, reactor)
</ins><span class="cx"> 
</span><del>-                for profileType in clientType.profileTypes:
-                    print(profileType)
-                    profile = profileType(reactor, self, client, number)
-                    if profile.enabled:
-                        d = profile.initialize()
-                        def _run(result):
-                            d2 = profile.run()
-                            d2.addErrback(self._profileFailure, profileType, reactor)
-                            return d2
-                        d.addCallback(_run)
</del><span class="cx"> 
</span><del>-        # XXX this status message is prone to be slightly inaccurate, but isn't
-        # really used by much anyway.
-        msg(type=&quot;status&quot;, clientCount=self._user - 1)
-
-        # for i in range(numClients):
-        #     for j in range(clientsPerUser):
-        #         client = self._pop.next()
-        #         # Reactor magic goes here
-        #         client.setUp(
-        #             self.reactor,
-        #             self.server,
-        #             self.serializationPath,
-        #             self.getUserRecord(number),
-        #             auth
-        #         )
-        #         for profile in client.profiles:
-        #             profile.setUp(self.reactor, self, client, number)
-
-
-
-
</del><span class="cx">     def _dumpLogs(self, loggingReactor, reason):
</span><span class="cx">         path = FilePath(mkdtemp())
</span><span class="cx">         logstate = loggingReactor.getLogFiles()
</span><span class="lines">@@ -297,7 +315,7 @@
</span><span class="cx">         return path
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def _profileFailure(self, reason, profileType, reactor):
</del><ins>+    def _profileFailure(self, reason, reactor):
</ins><span class="cx">         if not self._stopped:
</span><span class="cx">             where = self._dumpLogs(reactor, reason)
</span><span class="cx">             err(reason, &quot;Profile stopped with error; recent traffic in %r&quot; % (
</span><span class="lines">@@ -321,17 +339,16 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class SmoothRampUp(object):
</span><del>-    def __init__(self, reactor, groups, groupSize, interval, clientsPerUser):
-        self.reactor = reactor
</del><ins>+    def __init__(self, groups, groupSize, interval, clientsPerUser):
</ins><span class="cx">         self.groups = groups
</span><span class="cx">         self.groupSize = groupSize
</span><span class="cx">         self.interval = interval
</span><span class="cx">         self.clientsPerUser = clientsPerUser
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def run(self, simulator):
</del><ins>+    def run(self, reactor, simulator):
</ins><span class="cx">         for i in range(self.groups):
</span><del>-            self.reactor.callLater(
</del><ins>+            reactor.callLater(
</ins><span class="cx">                 self.interval * i, simulator.add, self.groupSize, self.clientsPerUser)
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestprofilespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/profiles.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -23,6 +23,7 @@
</span><span class="cx"> 
</span><span class="cx"> import random
</span><span class="cx"> from uuid import uuid4
</span><ins>+from numbers import Number
</ins><span class="cx"> 
</span><span class="cx"> from caldavclientlibrary.protocol.caldav.definitions import caldavxml
</span><span class="cx"> 
</span><span class="lines">@@ -37,7 +38,7 @@
</span><span class="cx"> 
</span><span class="cx"> from contrib.performance.loadtest.distributions import (
</span><span class="cx">     NearFutureDistribution, NormalDistribution, UniformDiscreteDistribution, BernoulliDistribution,
</span><del>-    LogNormalDistribution, RecurrenceDistribution
</del><ins>+    LogNormalDistribution, RecurrenceDistribution, FixedDistribution
</ins><span class="cx"> )
</span><span class="cx"> from contrib.performance.loadtest.ical import IncorrectResponseCode
</span><span class="cx"> from contrib.performance.loadtest.resources import Calendar, Event
</span><span class="lines">@@ -47,6 +48,23 @@
</span><span class="cx"> from pycalendar.duration import Duration
</span><span class="cx"> from pycalendar.value import Value
</span><span class="cx"> 
</span><ins>+def _loopWithDistribution(reactor, distribution, function):
+    result = Deferred()
+    print(distribution)
+
+    def repeat(ignored):
+        reactor.callLater(distribution.sample(), iterate)
+
+    def iterate():
+        d = function()
+        if d is not None:
+            d.addCallbacks(repeat, result.errback)
+        else:
+            repeat(None)
+
+    repeat(None)
+    return result
+
</ins><span class="cx"> class ProfileBase(object):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Base class which provides some conveniences for profile
</span><span class="lines">@@ -55,11 +73,19 @@
</span><span class="cx">     random = random
</span><span class="cx"> 
</span><span class="cx">     def __init__(self, enabled, interval, **params):
</span><ins>+        print(&quot;Creating new profile: %s&quot; % (self.__class__.__name__,))
</ins><span class="cx">         self.enabled = enabled
</span><ins>+        if isinstance(interval, Number):
+            interval = FixedDistribution(interval)
</ins><span class="cx">         self._interval = interval
</span><del>-        self.setParameters(**params)
</del><ins>+        print &quot;**&quot; + str(self._interval)
+        self._params = params
+        self.setDistributions(**params)
</ins><span class="cx">         self._initialized = False
</span><span class="cx"> 
</span><ins>+    def duplicate(self):
+        return type(self)(enabled=self.enabled, interval=self._interval, **self._params)
+
</ins><span class="cx">     def setUp(self, reactor, simulator, client, record):
</span><span class="cx">         self._reactor = reactor
</span><span class="cx">         self._sim = simulator
</span><span class="lines">@@ -67,49 +93,19 @@
</span><span class="cx">         self._record = record
</span><span class="cx">         self._initialized = True
</span><span class="cx"> 
</span><del>-    def setParameters(self):
</del><ins>+    def setDistributions(self):
</ins><span class="cx">         pass
</span><span class="cx"> 
</span><ins>+    def _wrapper(self):
+        if not self.enabled:
+            return succeed(None)
+        if not self._client.started:
+            return succeed(None)
+        return self.action()
+
</ins><span class="cx">     def run(self):
</span><del>-        # def action(self):
-        #     if self.enabled:
-        #         return self.action()
</del><ins>+        return _loopWithDistribution(self._reactor, self._interval, self._wrapper)
</ins><span class="cx"> 
</span><del>-        print(&quot;Hello from run&quot;)
-        _call = LoopingCall(self.action)
-        _call.clock = self._reactor
-        return _call.start(self._interval)
-
-        # OR
-
-    #     return self._loopWithDistribution(
-    #         self._reactor, self._sendInvitationDistribution, self._invite)
-
-    # def _loopWithDistribution(reactor, distribution, function):
-    #     result = Deferred()
-
-    #     def repeat(ignored):
-    #         reactor.callLater(distribution.sample(), iterate)
-
-    #     def iterate():
-    #         d = function()
-    #         if d is not None:
-    #             d.addCallbacks(repeat, result.errback)
-    #         else:
-    #             repeat(None)
-
-    #     repeat(None)
-    #     return result
-
-    def initialize(self):
-        &quot;&quot;&quot;
-        Called before the profile runs for real. Can be used to initialize client state.
-
-        @return: a L{Deferred} that fires when initialization is done
-        &quot;&quot;&quot;
-        return succeed(None)
-
-
</del><span class="cx">     def _calendarsOfType(self, calendarType, componentType):
</span><span class="cx">         return [
</span><span class="cx">             cal
</span><span class="lines">@@ -127,12 +123,12 @@
</span><span class="cx">         return attendee.parameterValue('EMAIL') == self._client.email[len('mailto:'):]
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def _getRandomCalendar(self):
</del><ins>+    def _getRandomCalendarOfType(self, component_type):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return a random L{Calendar} object from the current user
</span><span class="cx">         or C{None} if there are no calendars to work with
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        calendars = self._calendarsOfType(caldavxml.calendar, &quot;VEVENT&quot;)
</del><ins>+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
</ins><span class="cx">         if not calendars: # Oh no! There are no calendars to play with
</span><span class="cx">             return None
</span><span class="cx">         # Choose a random calendar
</span><span class="lines">@@ -140,12 +136,12 @@
</span><span class="cx">         return calendar
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def _getRandomEvent(self):
</del><ins>+    def _getRandomEventOfType(self, component_type):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Return a random L{Event} object from the current user
</span><span class="cx">         or C{None} if there are no events to work with
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        calendars = self._calendarsOfType(caldavxml.calendar, &quot;VEVENT&quot;)
</del><ins>+        calendars = self._calendarsOfType(caldavxml.calendar, component_type)
</ins><span class="cx">         while calendars:
</span><span class="cx">             calendar = self.random.choice(calendars)
</span><span class="cx">             calendars.remove(calendar)
</span><span class="lines">@@ -232,50 +228,42 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     pass
</span><span class="cx"> 
</span><del>-    &quot;&quot;&quot; Event-Interaction Profiles
-Event Creation - Eventer
-Event Changing - EventUpdaterBase
-                    TitlerMixin
-                    RelocaterMixin
-                    ReschedulerMixin
-                    RepeaterMixin
-                    AlerterMixin
-                    InviterMixin
-                    NoterMixin
-                    InviterMixin
-                    LinkerMixin
-                    AttacherMixin
-Event Deletion - EventerDeleter
-&quot;&quot;&quot;
</del><span class="cx"> 
</span><span class="cx"> #####################
</span><del>-# Eventer Behaviors #
</del><ins>+# Eventer Hierarchy #
+# ----------------- #
+# EventBase         #
+#   Eventer         #
+#   EventUpdaterBase#
+#     Titler        #
+#     Noter         #
+#     Linker        #
+#     Relocater     #
+#     Repeater      #
+#     Rescheduler   #
+#     ~Alerter~     #
+#     Attacher      #
+#     InviterBase   #
+#       Inviter     #
+#       Relocater   #
+#   EventDeleter    #
</ins><span class="cx"> #####################
</span><span class="cx"> 
</span><del>-class EventerBase(ProfileBase):
</del><ins>+class EventBase(ProfileBase):
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Base profile for a calendar user who interacts with events
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    def setParameters(
-        self,
-        enabled=True,
-        interval=25,
-        **params
-    ):
-        self.enabled = enabled
-        self._interval = interval
-        self.setDistributions(**params)
</del><ins>+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VEVENT')
</ins><span class="cx"> 
</span><ins>+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VEVENT')
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-class Eventer(EventerBase):
</del><ins>+class Eventer(EventBase):
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     A Calendar user who creates new events.
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    def initialize(self):
-        self.action = self._addEvent
-        return succeed(None)
-
</del><span class="cx">     def setDistributions(
</span><span class="cx">         self,
</span><span class="cx">         eventStartDistribution=NearFutureDistribution(),
</span><span class="lines">@@ -285,30 +273,25 @@
</span><span class="cx">             120 * 60
</span><span class="cx">         ])
</span><span class="cx">     ):
</span><del>-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
</del><ins>+        &quot;&quot;&quot;
+        @param eventStartDistribution: Generates datetimes at which an event starts
+        @param eventDurationDistribution: Generates length of event (in seconds)
+        &quot;&quot;&quot;
+        self._eventStart = eventStartDistribution
+        self._eventDuration = eventDurationDistribution
</ins><span class="cx"> 
</span><span class="cx">     def _addEvent(self):
</span><del>-        print &quot;Hello a bit&quot;
-
-        if not self._client.started:
-            return succeed(None)
-
-
-
</del><ins>+        # Choose a random calendar on which to add an event
</ins><span class="cx">         calendar = self._getRandomCalendar()
</span><span class="cx">         if not calendar:
</span><span class="cx">             return succeed(None)
</span><span class="cx"> 
</span><del>-        print &quot;Made it&quot;
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
</del><ins>+        # Form a new event by modifying fields of the template event
</ins><span class="cx">         vcalendar = eventTemplate.duplicate()
</span><span class="cx">         vevent = vcalendar.mainComponent()
</span><span class="cx">         uid = str(uuid4())
</span><del>-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
</del><ins>+        dtstart = self._eventStart.sample()
+        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
</ins><span class="cx"> 
</span><span class="cx">         vevent.replaceProperty(Property(&quot;UID&quot;, uid))
</span><span class="cx">         vevent.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
</span><span class="lines">@@ -319,17 +302,18 @@
</span><span class="cx">         href = '%s%s.ics' % (calendar.url, uid)
</span><span class="cx">         event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
</span><span class="cx">         d = self._client.addEvent(href, event)
</span><del>-        return self._newOperation(&quot;create&quot;, d)
</del><ins>+        return self._newOperation(&quot;create{event}&quot;, d)
</ins><span class="cx"> 
</span><ins>+    action = _addEvent
+
</ins><span class="cx"> # Could have better handling for not changing events once they're modified
</span><span class="cx"> # esp re: repeating
</span><del>-class EventUpdaterBase(EventerBase):
</del><ins>+class EventUpdaterBase(EventBase):
</ins><span class="cx">     &quot;&quot;&quot;Superclass of all event mixins.
</span><span class="cx">     Accepts two parameters
</span><span class="cx">     enabled: bool on or off
</span><span class="cx">     interval: distibution that generates integers representing delays
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    # COMPONENT_TYPE = None
</del><span class="cx">     def action(self):
</span><span class="cx">         event = self._getRandomEvent()
</span><span class="cx">         if not event:
</span><span class="lines">@@ -346,7 +330,7 @@
</span><span class="cx"> 
</span><span class="cx">         return self._newOperation(label, d)
</span><span class="cx"> 
</span><del>-    def modifyEvent(self):
</del><ins>+    def modifyEvent(self, href, vevent):
</ins><span class="cx">         &quot;&quot;&quot;Overriden by subclasses&quot;&quot;&quot;
</span><span class="cx">         pass
</span><span class="cx"> 
</span><span class="lines">@@ -450,19 +434,21 @@
</span><span class="cx">         return &quot;reschedule{event}&quot;
</span><span class="cx"> 
</span><span class="cx"> # class Alerter(EventUpdaterBase):
</span><ins>+# component.replaceProperty(Property(&quot;ACKNOWLEDGED&quot;, DateTime.getNowUTC()))
</ins><span class="cx"> #     pass
</span><span class="cx"> 
</span><span class="cx"> class Attacher(EventUpdaterBase):
</span><span class="cx">     def setDistributions(
</span><span class="cx">         self,
</span><ins>+        filesizeDistribution=NormalDistribution(24, 3),
+        numAttachmentsDistribution=LogNormalDistribution(2, 1),
+        attachLikelihoodDistribution=BernoulliDistribution(0.9),
+
</ins><span class="cx">     ):
</span><del>-    # filesizeDistribution=NormalDistribution(24, 3),
-    # numAttachmentsDistribution=LogNormalDistribution(2, 1),
-    # attachLikelihoodDistribution=BernoulliDistribution(0.9)
-        # self._filesize = filesizeDistribution
</del><ins>+        self._filesize = filesizeDistribution
</ins><span class="cx">         # self._numAttachments = numAttachmentsDistribution
</span><span class="cx">         # self._attachLikelihood = attachLikelihoodDistribution
</span><del>-        pass
</del><ins>+        # pass
</ins><span class="cx"> 
</span><span class="cx">     def modifyEvent(self, href, vevent):
</span><span class="cx">         d = self._client.postAttachment(href, 'x' * 1024)
</span><span class="lines">@@ -487,405 +473,52 @@
</span><span class="cx">     def unattachFile(self):
</span><span class="cx">         pass
</span><span class="cx"> 
</span><del>-class InviterBase(EventerBase):
-    &quot;&quot;&quot;
-    Base profile for a calendar user that invites and deinvites other principals to events
-    &quot;&quot;&quot;
-    def setParameters(
</del><ins>+class InviterBase(EventUpdaterBase):
+    def setDistributions(
</ins><span class="cx">         self,
</span><del>-        enabled=True,
</del><ins>+        numInvitees=NormalDistribution(7, 2),
</ins><span class="cx">         sendInvitationDistribution=NormalDistribution(600, 60),
</span><del>-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11)),
-        **params
</del><ins>+        numInviteesDistribution=UniformDiscreteDistribution(range(-10, 11))
</ins><span class="cx">     ):
</span><span class="cx">         self.enabled = enabled
</span><span class="cx">         self._sendInvitationDistribution = sendInvitationDistribution
</span><del>-        self._inviteeDistribution = inviteeDistribution
-        if len(params) &gt; 0:
-            pass
</del><ins>+        self._numInvitees = inviteeDistribution
</ins><span class="cx"> 
</span><del>-    def getAttendees():
</del><ins>+    def _findUninvitedRecord(self, vevent):
</ins><span class="cx">         pass
</span><span class="cx"> 
</span><del>-    # def _invitePrincipal(self, ...):
</del><ins>+    def _addAttendee(self, some_id):
+        attendeeProp = self._buildAttendee()
+        self._client.attendeeAutocomplete
</ins><span class="cx"> 
</span><del>-    # def _uninvitePrincipal(self, ...):
-
-
-
-
-    def _loopWithDistribution(reactor, distribution, function):
-        result = Deferred()
-
-        def repeat(ignored):
-            reactor.callLater(distribution.sample(), iterate)
-
-        def iterate():
-            d = function()
-            if d is not None:
-                d.addCallbacks(repeat, result.errback)
-            else:
-                repeat(None)
-
-        repeat(None)
-        return result
-
</del><span class="cx">     # def _didSelfOrganize(self, vevent):
</span><span class="cx"> 
</span><ins>+    # TODO handle alternate roles
+    def _buildAttendee(self, commonname, cuaddr, isIndividual=True, isRequired=False):
+        return Property(
+            name=u'ATTENDEE',
+            value=cuaddr.encode(&quot;utf-8&quot;),
+            params={
+                'CN': commonname,
+                'CUTYPE': 'INDIVIDUAL' if isIndividual else 'ROOM',
+                'PARTSTAT': 'NEEDS-ACTION',
+                'ROLE': 'REQ-PARTICIPANT',
+                'RSVP': 'TRUE',
+            },
+        )
</ins><span class="cx"> 
</span><del>-    # def _buildIndividualAttendee(self, commonName, record, ):
</del><ins>+    def _getAttendees(self, vevent):
+        return vevent.properties('ATTENDEE')
</ins><span class="cx"> 
</span><del>-    #     # ATTENDEE;CN=&quot;Super User&quot;;CUTYPE=INDIVIDUAL;EMAIL=&quot;admin@example.com&quot;:mailto:admin@example.com
-    #     # ATTENDEE;CN=&quot;User 04&quot;;CUTYPE=INDIVIDUAL;EMAIL=&quot;user04@example.com&quot;:mailto:user04@example.com
-
-    #     role = ['REQ-PARTICIPANT', '']
-    #     attendee = Property(
-    #         name=u'ATTENDEE',
-    #         value=cuaddr.encode(&quot;utf-8&quot;),
-    #         params={
-    #             'CN': commonName,
-    #             'CUTYPE': 'INDIVIDUAL',
-    #             'PARTSTAT': 'NEEDS-ACTION',
-    #             'ROLE': 'REQ-PARTICIPANT',
-    #             'RSVP': 'TRUE',
-    #         },
-    #     )
-    #     return attendee
-
-    # def _buildLocationAttendee(self, cn, cuaddr):
-    #     &quot;&quot;&quot;
-    #     Example Usage: profile._buildLocationAttendee(&quot;Location 01&quot;, &quot;urn:uuid:...&quot;)
-    #     &quot;&quot;&quot;
-    #     role = ['REQ-PARTICIPANT', '']
-    #     attendee = Property(
-    #         name=u'ATTENDEE',
-    #         value=cuaddr.encode(&quot;utf-8&quot;),
-    #         params={
-    #             'CN': commonName,
-    #             'CUTYPE': 'ROOM',
-    #             'PARTSTAT': 'NEEDS-ACTION',
-    #             'ROLE': 'REQ-PARTICIPANT',
-    #             'RSVP': 'TRUE',
-    #         },
-    #     )
-    #     return attendee
-
</del><span class="cx">     def _invite():
</span><span class="cx">         raise NotImplementedError
</span><span class="cx"> 
</span><span class="cx">     def _addAttendee():
</span><span class="cx">         raise NotImplementedError
</span><span class="cx"> 
</span><del>-# TODO - invite groups
-# class Inviter(EventUpdaterBase): pass
</del><ins>+class Inviter(InviterBase):
</ins><span class="cx"> 
</span><del>-class Relocater(EventUpdaterBase):
-    def setParameters(
-        self,
-    ):
-        pass
-
-class AppleEventer(EventerBase):
-    &quot;&quot;&quot;
-    Calendar user who makes events in the form of Apple logo
-    &quot;&quot;&quot;
</del><span class="cx">     def initialize(self):
</span><del>-        self.action = self._addEvent
-        return succeed(None)
-
-    def setDistributions(
-        self
-    ):
-        pass
-
-    def _addEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendar = self._getRandomCalendar()
-        if not calendar:
-            return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-
-        vevent.replaceProperty(Property(&quot;UID&quot;, uid))
-        vevent.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-        vevent.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-        vevent.replaceProperty(Property(&quot;DTSTART&quot;, dtstart))
-        vevent.replaceProperty(Property(&quot;DTEND&quot;, dtend))
-
-        href = '%s%s.ics' % (calendar.url, uid)
-        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
-        d = self._client.addEvent(href, event)
-        return self._newOperation(&quot;create&quot;, d)
-
-class HappyEventer(EventerBase):
-    &quot;&quot;&quot;
-    Calendar user who makes events in the form of Apple logo
-    &quot;&quot;&quot;
-    def initialize(self):
-        self.action = self._addEvent
-        return succeed(None)
-
-    def setDistributions(
-        self
-    ):
-        self._eventStart = UniformDiscreteDistribution([
-            # August 3
-            DateTime(year=2015, month=8, day=3, hours=15, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=15, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=3, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            # August 4
-            DateTime(year=2015, month=8, day=4, hours=10, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=10, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=11, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=11, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            DateTime(year=2015, month=8, day=4, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=4, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-
-            # August 5
-            DateTime(year=2015, month=8, day=5, hours=13, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=13, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=5, hours=17, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            # August 6
-            DateTime(year=2015, month=8, day=6, hours=10, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=10, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=11, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=11, minutes=30, seconds=0, tzid=None, utcoffset=None),
-
-            DateTime(year=2015, month=8, day=6, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=6, hours=17, minutes=0, seconds=0, tzid=None, utcoffset=None),
-
-            # August 7
-            DateTime(year=2015, month=8, day=7, hours=15, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=15, minutes=30, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=16, minutes=0, seconds=0, tzid=None, utcoffset=None),
-            DateTime(year=2015, month=8, day=7, hours=16, minutes=30, seconds=0, tzid=None, utcoffset=None),
-        ])
-        self._eventDuration = UniformDiscreteDistribution([
-            30 * 60,
-            60 * 60
-        ])
-
-    def _addEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendar = self._getRandomCalendar()
-        if not calendar:
-            return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStart.sample()
-        dtend = dtstart + Duration(seconds=self._eventDuration.sample())
-
-        vevent.replaceProperty(Property(&quot;UID&quot;, uid))
-        vevent.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-        vevent.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-
-        vevent.replaceProperty(Property(&quot;DTSTART&quot;, dtstart))
-        vevent.replaceProperty(Property(&quot;DTEND&quot;, dtend))
-
-        href = '%s%s.ics' % (calendar.url, uid)
-        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
-        d = self._client.addEvent(href, event)
-        return self._newOperation(&quot;create&quot;, d)
-
-
-
-# class EventDeleter(ProfileBase):
-
-
-
-&quot;&quot;&quot; TEST &quot;&quot;&quot;
-# class Intern(object):
-#     def __init__(self):
-#         self.behaviors = [
-#             Eventer(asdfjadsf),
-#             Attacher(asjadsfjasdf),
-#             Inviter(enabled=True, **params)
-#         ]
-
-#     def run(self):
-#         deferreds = []
-#         for behavior in self.behaviors:
-#             deferreds.append(behavior.run())
-#         return DeferredList(deferreds)
-
-
-
-####################
-# Class Hierarchy
-# ---------------
-# TaskBase
-#   Tasker
-#   TaskUpdaterBase
-#     Titler
-#     Alerter
-#     Noter
-#     Prioritizer
-#     Completer
-#   TaskDeleter
-####################
-class TaskBase(ProfileBase):
-    &quot;&quot;&quot;
-    Base profile for a calendar user who interacts with tasks
-    &quot;&quot;&quot;
-    def setParameters(self, enabled=True, interval=25, **params):
-        self.enabled = enabled
-        self._interval = interval
-
-class Tasker(TaskBase):
-    &quot;&quot;&quot;
-    A Calendar user who creates new tasks.
-    &quot;&quot;&quot;
-    def initialize(self):
-        self.action = self._addTask
-        return succeed(None)
-
-    def _addTask(self, title=&quot;Simple Task&quot;):
-        if not self._client.started:
-            return succeed(None)
-
-        calendars = self._calendarsOfType(caldavxml.calendar, &quot;VTODO&quot;)
-
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
-
-            # Copy the template task and fill in some of its fields
-            # to make a new task to create on the calendar.
-            vcalendar = taskTemplate.duplicate()
-            vtodo = vcalendar.mainComponent()
-            uid = str(uuid4())
-            vtodo.replaceProperty(Property(&quot;UID&quot;, uid))
-            vtodo.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation(&quot;create&quot;, d)
-
-
-class TaskUpdaterBase(TaskBase):
-    def action(self):
-        task = self._getRandomTask()
-        if not task:
-            return succeed(None)
-        component = task.component
-        vtodo = component.mainComponent()
-
-        label = self.modifyEvent(task.url, vtodo)
-        vtodo.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-
-        task.component = component
-        d = self._client.updateEvent(task)
-        return self._newOperation(label, d)
-
-    def modifyEvent(self):
-        &quot;&quot;&quot;Overriden by subclasses&quot;&quot;&quot;
-        pass
-
-
-
-
-class TaskUpdater(TaskBase):
-    &quot;&quot;&quot;
-    A Calendar user who creates and updates complex tasks with:
-      Mark as completed/not completed
-      Change name
-      Change priority
-      Change notes
-      Sets due dates
-    &quot;&quot;&quot;
-
-    def setParameters(
-        self,
-        taskDueDistribution=NearFutureDistribution(),
-    ):
-        pass
-
-
-    def _addTask(self):
-        if not self._client.started:
-            return succeed(None)
-
-        calendars = self._calendarsOfType(caldavxml.calendar, &quot;VTODO&quot;)
-
-        while calendars:
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
-
-            # Copy the template task and fill in some of its fields
-            # to make a new task to create on the calendar.
-            vcalendar = taskTemplate.duplicate()
-            vtodo = vcalendar.mainComponent()
-            uid = str(uuid4())
-            due = self._taskStartDistribution.sample()
-            vtodo.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-            vtodo.replaceProperty(Property(&quot;UID&quot;, uid))
-            
-            # vtodo.replaceProperty(Property(&quot;SUMMARY&quot;, title))
-
-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addEvent(href, vcalendar)
-            return self._newOperation(&quot;create&quot;, d)
-
-
-
-# class TaskTitlerMixin = TitlerMixin
-# class TaskAlerterMixin = AlerterMixin
-# self._taskStartDistribution = taskDueDistribution
-# vtodo.replaceProperty(Property(&quot;DUE&quot;, due))
-# class TaskNoterMixin = NoterMixin
-
-class Prioritizer(TaskBase):
-    PRIORITY_NONE = 0
-    PRIORITY_HIGH = 1
-    PRIORITY_MEDIUM = 5
-    PRIORITY_LOW = 9
-
-    def _setPriority(self, priority, vtodo):
-        vtodo.replaceProperty(Property(&quot;PRIORITY&quot;, priority))
-
-class Completer(TaskBase):
-    def _markTaskComplete(vtodo):
-        &quot;&quot;&quot; task is a Component representing a VTODO &quot;&quot;&quot;
-        vtodo.replaceProperty(Property(&quot;COMPLETED&quot;, DateTime.getNowUTC()))
-        vtodo.replaceProperty(Property(&quot;PERCENT-COMPLETE&quot;, 100))
-        vtodo.replaceProperty(Property(&quot;STATUS&quot;, &quot;COMPLETED&quot;))
-
-
-    def _markTaskIncomplete(vtodo):
-        &quot;&quot;&quot; mark a VTODO as incomplete &quot;&quot;&quot;
-        vtodo.removeProperty(&quot;COMPLETED&quot;)
-        vtodo.removeProperty(&quot;PERCENT-COMPLETE&quot;)
-        vtodo.replaceProperty(Property(&quot;STATUS&quot;, &quot;NEEDS-ACTION&quot;))
-
-# class TaskDeleter(ProfileBase): pass
-
-
-class Inviter(ProfileBase):
-
-    def initialize(self):
</del><span class="cx">         self.action = self.test
</span><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><span class="lines">@@ -1014,135 +647,198 @@
</span><span class="cx">         # Oops, either no events or no calendars to play with.
</span><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><ins>+class Relocater(InviterBase):
+    def setDistributions(
+        self,
+    ):
+        pass
</ins><span class="cx"> 
</span><ins>+class EventDeleter(EventBase):
+    &quot;&quot;&quot;
+    A calendar user who deletes events at random
+    &quot;&quot;&quot;
+    def _deleteEvent(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
+        d = self._client.deleteEvent(event.url)
+        return self._newOperation(&quot;delete{event}&quot;, d)
</ins><span class="cx"> 
</span><del>-class RealisticInviter(ProfileBase):
</del><ins>+    action = _deleteEvent
+
+
+
+
+&quot;&quot;&quot; TEST &quot;&quot;&quot;
+# class Intern(object):
+#     def __init__(self):
+#         self.behaviors = [
+#             Eventer(asdfjadsf),
+#             Attacher(asjadsfjasdf),
+#             Inviter(enabled=True, **params)
+#         ]
+
+#     def run(self):
+#         deferreds = []
+#         for behavior in self.behaviors:
+#             deferreds.append(behavior.run())
+#         return DeferredList(deferreds)
+
+
+
+####################
+# Tasker Hierarchy #
+# ---------------- #
+# TaskBase         #
+#   Tasker         #
+#   TaskDeleter    #
+#   TaskUpdaterBase#
+#     Titler       #
+#     Noter        #
+#     Prioritizer  #
+#     Completer    #
+#     Alerter      #
+####################
+
+
+class TaskBase(ProfileBase):
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    A Calendar user who invites other users to new events.
</del><ins>+    Base profile for a calendar user who interacts with tasks
</ins><span class="cx">     &quot;&quot;&quot;
</span><del>-    def setParameters(
-        self,
-        enabled=True,
-        sendInvitationDistribution=NormalDistribution(600, 60),
-        inviteeDistribution=UniformDiscreteDistribution(range(-10, 11)),
-        inviteeClumping=True,
-        inviteeCountDistribution=LogNormalDistribution(1.2, 1.2),
-        eventStartDistribution=NearFutureDistribution(),
-        eventDurationDistribution=UniformDiscreteDistribution([
-            15 * 60, 30 * 60,
-            45 * 60, 60 * 60,
-            120 * 60
-        ]),
-        recurrenceDistribution=RecurrenceDistribution(False),
-    ):
-        self.enabled = enabled
-        self._sendInvitationDistribution = sendInvitationDistribution
-        self._inviteeDistribution = inviteeDistribution
-        self._inviteeClumping = inviteeClumping
-        self._inviteeCountDistribution = inviteeCountDistribution
-        self._eventStartDistribution = eventStartDistribution
-        self._eventDurationDistribution = eventDurationDistribution
-        self._recurrenceDistribution = recurrenceDistribution
</del><ins>+    def _getRandomCalendar(self):
+        return self._getRandomCalendarOfType('VTODO')
</ins><span class="cx"> 
</span><ins>+    def _getRandomEvent(self):
+        return self._getRandomEventOfType('VTODO')
</ins><span class="cx"> 
</span><del>-    def _addAttendee(self, event, attendees):
-        &quot;&quot;&quot;
-        Create a new attendee to add to the list of attendees for the
-        given event.
-        &quot;&quot;&quot;
-        selfRecord = self._sim.getUserRecord(self._number)
-        invitees = set([u'mailto:%s' % (selfRecord.email,)])
-        for att in attendees:
-            invitees.add(att.value())
</del><span class="cx"> 
</span><del>-        for _ignore_i in range(10):
</del><ins>+class Tasker(TaskBase):
+    &quot;&quot;&quot;
+    A Calendar user who creates new tasks.
+    &quot;&quot;&quot;
+    def _addTask(self):
+        calendar = self._getRandomCalendar()
+        if not calendar:
+            return succeed(None)
</ins><span class="cx"> 
</span><del>-            sample = self._inviteeDistribution.sample()
-            if self._inviteeClumping:
-                sample = self._number + sample
-            invitee = max(0, sample)
</del><ins>+        # Form a new event by modifying fields of the template event
+        vcalendar = taskTemplate.duplicate()
+        vtodo = vcalendar.mainComponent()
+        uid = str(uuid4())
</ins><span class="cx"> 
</span><del>-            try:
-                record = self._sim.getUserRecord(invitee)
-            except IndexError:
-                continue
-            cuaddr = u'mailto:%s' % (record.email,)
-            if cuaddr not in invitees:
-                break
-        else:
-            raise CannotAddAttendee(&quot;Can't find uninvited user to invite.&quot;)
</del><ins>+        vtodo.replaceProperty(Property(&quot;UID&quot;, uid))
+        vtodo.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
</ins><span class="cx"> 
</span><del>-        attendee = Property(
-            name=u'ATTENDEE',
-            value=cuaddr.encode(&quot;utf-8&quot;),
-            params={
-                'CN': record.commonName,
-                'CUTYPE': 'INDIVIDUAL',
-                'PARTSTAT': 'NEEDS-ACTION',
-                'ROLE': 'REQ-PARTICIPANT',
-                'RSVP': 'TRUE',
-            },
-        )
</del><ins>+        href = '%s%s.ics' % (calendar.url, uid)
+        event = Event(self._client.serializeLocation(), href, None, component=vcalendar)
+        d = self._client.addEvent(href, event)
+        return self._newOperation(&quot;create{task}&quot;, d)
</ins><span class="cx"> 
</span><del>-        event.addProperty(attendee)
-        attendees.append(attendee)
</del><ins>+    action = _addTask
</ins><span class="cx"> 
</span><ins>+class TaskDeleter(TaskBase):
+    def _deleteTask(self):
+        event = self._getRandomEvent()
+        if event is None:
+            return succeed(None)
</ins><span class="cx"> 
</span><del>-    def _invite(self):
-        &quot;&quot;&quot;
-        Try to add a new event, or perhaps remove an
-        existing attendee from an event.
</del><ins>+        d = self._client.deleteEvent(event.url)
+        return self._newOperation(&quot;delete{task}&quot;, d)
</ins><span class="cx"> 
</span><del>-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        &quot;&quot;&quot;
</del><ins>+    action = _deleteTask
</ins><span class="cx"> 
</span><del>-        if not self._client.started:
</del><ins>+
+class TaskUpdaterBase(TaskBase):
+    def action(self):
+        task = self._getRandomEvent()
+        if not task:
</ins><span class="cx">             return succeed(None)
</span><ins>+        component = task.component
+        vtodo = component.mainComponent()
</ins><span class="cx"> 
</span><del>-        # Find calendars which are eligible for invites
-        calendars = self._calendarsOfType(caldavxml.calendar, &quot;VEVENT&quot;)
</del><ins>+        label = self.modifyEvent(task.url, vtodo)
+        vtodo.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
</ins><span class="cx"> 
</span><del>-        while calendars:
-            # Pick one at random from which to try to create an event
-            # to modify.
-            calendar = self.random.choice(calendars)
-            calendars.remove(calendar)
</del><ins>+        task.component = component
+        d = self._client.updateEvent(task, method_label=&quot;update{task}&quot;)
+        return self._newOperation(label, d)
</ins><span class="cx"> 
</span><del>-            # Copy the template event and fill in some of its fields
-            # to make a new event to create on the calendar.
-            vcalendar = eventTemplate.duplicate()
-            vevent = vcalendar.mainComponent()
-            uid = str(uuid4())
-            dtstart = self._eventStartDistribution.sample()
-            dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-            vevent.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-            vevent.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-            vevent.replaceProperty(Property(&quot;DTSTART&quot;, dtstart))
-            vevent.replaceProperty(Property(&quot;DTEND&quot;, dtend))
-            vevent.replaceProperty(Property(&quot;UID&quot;, uid))
</del><ins>+    def modifyEvent(self, href, vtodo):
+        &quot;&quot;&quot;Overriden by subclasses&quot;&quot;&quot;
+        pass
</ins><span class="cx"> 
</span><del>-            rrule = self._recurrenceDistribution.sample()
-            if rrule is not None:
-                vevent.addProperty(Property(None, None, None, pycalendar=rrule))
</del><span class="cx"> 
</span><del>-            vevent.addProperty(self._client._makeSelfOrganizer())
-            vevent.addProperty(self._client._makeSelfAttendee())
</del><ins>+class TaskTitler(TaskUpdaterBase, Titler):
+    &quot;&quot;&quot;
+    Changes the SUMMARY of a random VTODO
+    &quot;&quot;&quot;
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property(&quot;SUMMARY&quot;, &quot;.&quot; * 5))
+        return &quot;update{title}&quot;
</ins><span class="cx"> 
</span><del>-            attendees = list(vevent.properties('ATTENDEE'))
-            for _ignore in range(int(self._inviteeCountDistribution.sample())):
-                try:
-                    self._addAttendee(vevent, attendees)
-                except CannotAddAttendee:
-                    self._failedOperation(&quot;invite&quot;, &quot;Cannot add attendee&quot;)
-                    return succeed(None)
</del><ins>+class TaskNoter(TaskUpdaterBase, Noter):
+    &quot;&quot;&quot;
+    Changes the NOTES of a random VTODO
+    &quot;&quot;&quot;
+    def modifyEvent(self, _ignore_href, vtodo):
+        vtodo.replaceProperty(Property(&quot;DESCRIPTION&quot;, &quot;.&quot; * 5))
+        return &quot;update{notes}&quot;
</ins><span class="cx"> 
</span><del>-            href = '%s%s.ics' % (calendar.url, uid)
-            d = self._client.addInvite(href, vcalendar)
-            return self._newOperation(&quot;invite&quot;, d)
</del><span class="cx"> 
</span><ins>+# class TaskAlerterMixin = AlerterMixin (alarm AND due)
+# self._taskStartDistribution = taskDueDistribution
+# vtodo.replaceProperty(Property(&quot;DUE&quot;, due))
</ins><span class="cx"> 
</span><ins>+
+class Prioritizer(TaskUpdaterBase):
+    PRIORITY_NONE = 0
+    PRIORITY_HIGH = 1
+    PRIORITY_MEDIUM = 5
+    PRIORITY_LOW = 9
+
+    def setDistributions(
+        self,
+        priorityDistribution=UniformDiscreteDistribution([
+            PRIORITY_NONE, PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH
+        ])
+    ):
+        self._priority = priorityDistribution
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        self._setPriority(vtodo, self._priority.sample())
+
+
+    def _setPriority(self, vtodo, priority):
+        &quot;&quot;&quot; Set the PRIORITY of a VTODO &quot;&quot;&quot;
+        vtodo.replaceProperty(Property(&quot;PRIORITY&quot;, priority))
+
+class Completer(TaskUpdaterBase):
+    def setDistributions(
+        self,
+        completeLikelihood=BernoulliDistribution(0.9)
+    ):
+        self._complete = completeLikelihood
+
+    def modifyEvent(self, _ignore_href, vtodo):
+        if self._complete.sample():
+            self._markTaskComplete(vtodo)
+        else:
+            self._markTaskIncomplete(vtodo)
+
+    def _markTaskComplete(self, vtodo):
+        &quot;&quot;&quot; Mark a VTODO as complete &quot;&quot;&quot;
+        vtodo.replaceProperty(Property(&quot;COMPLETED&quot;, DateTime.getNowUTC()))
+        vtodo.replaceProperty(Property(&quot;PERCENT-COMPLETE&quot;, 100))
+        vtodo.replaceProperty(Property(&quot;STATUS&quot;, &quot;COMPLETED&quot;))
+
+    def _markTaskIncomplete(self, vtodo):
+        &quot;&quot;&quot; Mark a VTODO as incomplete &quot;&quot;&quot;
+        vtodo.removeProperty(&quot;COMPLETED&quot;)
+        vtodo.removeProperty(&quot;PERCENT-COMPLETE&quot;)
+        vtodo.replaceProperty(Property(&quot;STATUS&quot;, &quot;NEEDS-ACTION&quot;))
+
+
</ins><span class="cx"> ##########################
</span><span class="cx"> # Notification Behaviors #
</span><span class="cx"> ##########################
</span><span class="lines">@@ -1151,7 +847,7 @@
</span><span class="cx">     A Calendar user who accepts invitations to events. As well as accepting requests, this
</span><span class="cx">     will also remove cancels and replies.
</span><span class="cx">     &quot;&quot;&quot;
</span><del>-    def setParameters(
</del><ins>+    def setDistributions(
</ins><span class="cx">         self,
</span><span class="cx">         enabled=True,
</span><span class="cx">         acceptDelayDistribution=NormalDistribution(1200, 60)
</span><span class="lines">@@ -1298,107 +994,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class EventUpdater(EventerBase):
-    &quot;&quot;&quot;
-    A Calendar user who creates a new event, and then updates its alarm.
-    &quot;&quot;&quot;
-    def initialize(self):
-        &quot;&quot;&quot;
-        Called before the profile runs for real. Can be used to initialize client state.
-
-        @return: a L{Deferred} that fires when initialization is done
-        &quot;&quot;&quot;
-        self.action = self._updateEvent
-        return self._initEvent()
-
-
-    def _initEvent(self):
-        if not self._client.started:
-            return succeed(None)
-
-        # If it already exists, don't re-create
-        calendar = self._calendarsOfType(caldavxml.calendar, &quot;VEVENT&quot;)[0]
-        if calendar.events:
-            events = [event for event in calendar.events.values() if event.url.endswith(&quot;event_to_update.ics&quot;)]
-            if events:
-                return succeed(None)
-
-        # Copy the template event and fill in some of its fields
-        # to make a new event to create on the calendar.
-        vcalendar = self._eventTemplate.duplicate()
-        vevent = vcalendar.mainComponent()
-        uid = str(uuid4())
-        dtstart = self._eventStartDistribution.sample()
-        dtend = dtstart + Duration(seconds=self._eventDurationDistribution.sample())
-        vevent.replaceProperty(Property(&quot;CREATED&quot;, DateTime.getNowUTC()))
-        vevent.replaceProperty(Property(&quot;DTSTAMP&quot;, DateTime.getNowUTC()))
-        vevent.replaceProperty(Property(&quot;DTSTART&quot;, dtstart))
-        vevent.replaceProperty(Property(&quot;DTEND&quot;, dtend))
-        vevent.replaceProperty(Property(&quot;UID&quot;, uid))
-
-        rrule = self._recurrenceDistribution.sample()
-        if rrule is not None:
-            vevent.addProperty(Property(None, None, None, pycalendar=rrule))
-
-        href = '%s%s' % (calendar.url, &quot;event_to_update.ics&quot;)
-        d = self._client.addEvent(href, vcalendar)
-        return self._newOperation(&quot;create&quot;, d)
-
-
-    def _updateEvent(self):
-        &quot;&quot;&quot;
-        Try to add a new attendee to an event, or perhaps remove an
-        existing attendee from an event.
-
-        @return: C{None} if there are no events to play with,
-            otherwise a L{Deferred} which fires when the attendee
-            change has been made.
-        &quot;&quot;&quot;
-
-        if not self._client.started:
-            return succeed(None)
-
-        # If it does not exist, try to create it
-        calendar = self._calendarsOfType(caldavxml.calendar, &quot;VEVENT&quot;)[0]
-        if not calendar.events:
-            return self._initEvent()
-        events = [event for event in calendar.events.values() if event.url.endswith(&quot;event_to_update.ics&quot;)]
-        if not events:
-            return self._initEvent()
-        event = events[0]
-
-        # Add/update the ACKNOWLEDGED property
-        component = event.component.mainComponent()
-        component.replaceProperty(Property(&quot;ACKNOWLEDGED&quot;, DateTime.getNowUTC()))
-        d = self._client.changeEvent(event.url)
-        return self._newOperation(&quot;update&quot;, d)
-
-    # def _changeEventTitle(self, event, title):
-    #     event.component = self._setEventTitle(event.component, title)
-        # event.
-
-
-
-
-
-class EventDeleter(EventerBase):
-    &quot;&quot;&quot;
-    A calendar user who deletes events at random
-    &quot;&quot;&quot;
-    def initialize(self):
-        self.action = self._deleteEvent
-
-    def _deleteEvent(self):
-        event = self._getRandomEvent()
-        if event is None:
-            return succeed(None)
-        d = self._client.deleteEvent(event)
-        return self._newOperation(&quot;delete&quot;, d)
-
-
-
-
-
</del><span class="cx"> ######################
</span><span class="cx"> # Calendar Behaviors #
</span><span class="cx"> ######################
</span><span class="lines">@@ -1411,7 +1006,7 @@
</span><span class="cx">         return succeed(None)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def setParameters(self, enabled=True, interval=25):
</del><ins>+    def setDistributions(self, enabled=True, interval=25):
</ins><span class="cx">         self.enabled = enabled
</span><span class="cx">         self._interval = interval
</span><span class="cx"> 
</span><span class="lines">@@ -1419,26 +1014,26 @@
</span><span class="cx"> 
</span><span class="cx"> class CalendarMaker(CalendarBase):
</span><span class="cx">     &quot;&quot;&quot; A Calendar user who adds new Calendars &quot;&quot;&quot;
</span><del>-    def initialize(self):
-        self.action = self._addCalendar
-        return succeed(None)
</del><span class="cx"> 
</span><del>-
</del><span class="cx">     def _addCalendar(self):
</span><del>-        if not self._client.started:
-            return None
</del><ins>+        print &quot;Adding a calendar&quot;
+        # if not self._client.started:
+        #     return None
</ins><span class="cx"> 
</span><del>-        uid = str(uuid4())
</del><ins>+        # uid = str(uuid4())
</ins><span class="cx"> 
</span><del>-        body = Calendar.buildCalendarXML(order=0, component_type=&quot;VEVENT&quot;, rgba_color='FB524FFF', name='Sample Calendar')
-        print(&quot;Making new calendar with uid: &quot; + uid)
-        # XXX Just for testing! remove this soon
-        path = &quot;/calendars/__uids__/&quot; + self._client.record.guid + &quot;/&quot; + uid + &quot;/&quot;
-        d = self._client.addCalendar(path, body)
</del><ins>+        # body = Calendar.buildCalendarXML(order=0, component_type=&quot;VEVENT&quot;, rgba_color='FB524FFF', name='Sample Calendar')
+        # print(&quot;Making new calendar with uid: &quot; + uid)
+        # # XXX Just for testing! remove this soon
+        # path = &quot;/calendars/__uids__/&quot; + self._client.record.guid + &quot;/&quot; + uid + &quot;/&quot;
+        # d = self._client.addCalendar(path, body)
+        d = succeed('calendar created')
</ins><span class="cx">         return self._newOperation(&quot;create&quot;, d)
</span><span class="cx"> 
</span><ins>+    action = _addCalendar
</ins><span class="cx"> 
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class CalendarUpdater(CalendarBase):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     A calendar user who updates random calendars
</span><span class="lines">@@ -1545,3 +1140,16 @@
</span><span class="cx">         print(&quot;Deleting &quot; + calendar.url)
</span><span class="cx">         d = self._client.deleteCalendar(calendar.url)
</span><span class="cx">         return self._newOperation(&quot;delete&quot;, d)
</span><ins>+
+if __name__ == '__main__':
+    class TestProfile(ProfileBase):
+        def sayHello(self):
+            print(&quot;Hello!&quot;)
+        action = sayHello
+
+    from twisted.internet import reactor
+
+    profile = TestProfile(enabled=True, interval=1)
+    profile.setUp(reactor, None, None, None)
+    profile.run()
+    reactor.run()
</ins></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestpushpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/push.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,6 +1,10 @@
</span><del>-from calendarserver.push.amppush import subscribeToIDs
-from twisted.internet.defer import succeed
</del><ins>+import uuid
</ins><span class="cx"> 
</span><ins>+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.internet.defer import inlineCallbacks
+
+from calendarserver.push.amppush import SubscribeToID, UnsubscribeFromID, AMPPushClientFactory
+
</ins><span class="cx"> class PushMonitor(object):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Watchguard that monitors push notifications (AMP Push)
</span><span class="lines">@@ -21,71 +25,107 @@
</span><span class="cx">         @param ampPushPort: AMP port to connect to (e.g. 62311)
</span><span class="cx">         @type ampPushPort: integer
</span><span class="cx">         @param callback: a one-argument function that is fired
</span><del>-            with a calendar hrefupon receipt of a push notification
</del><ins>+            with a calendar href upon receipt of a push notification
</ins><span class="cx">             for that resource
</span><span class="cx">         @type callback: one-argument callable
</span><span class="cx">         &quot;&quot;&quot;
</span><ins>+
+        if reactor is None:
+            from twisted.internet import reactor
+
</ins><span class="cx">         self._reactor = reactor
</span><span class="cx">         self._ampPushHost = ampPushHost
</span><span class="cx">         self._ampPushPort = ampPushPort
</span><span class="cx"> 
</span><span class="cx">         # Keep track of AMP parameters for calendar homes we encounter.  This
</span><del>-        # dictionary has calendar home URLs as keys and pushkeys as
-        # values.
</del><ins>+        # dictionary has pushkeys as keys and calendar home URLs as values.
</ins><span class="cx">         self._ampPushkeys = {}
</span><span class="cx"> 
</span><ins>+        self._callback = callback
+
+        self._token = str(uuid.uuid4()) # Unique token for this monitor
+        self._endpoint = TCP4ClientEndpoint(self._reactor, self._ampPushHost, self._ampPushPort)
+        self._factory = AMPPushClientFactory(self._receivedAMPPush)
+        self._connected = False
+
+    @inlineCallbacks
</ins><span class="cx">     def begin(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Start monitoring for AMP-based push notifications
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        self._subscribeToIDs(self.ampPushkeys)
</del><ins>+        self._protocol = yield self._endpoint.connect(self._factory)
+        self._connected = True
+        pushkeys = self._ampPushkeys.keys()
+        yield self._subscribeToPushkeys(pushkeys)
</ins><span class="cx"> 
</span><ins>+    @inlineCallbacks
</ins><span class="cx">     def end(self):
</span><span class="cx">         &quot;&quot;&quot;
</span><del>-        Finish monitoring push notifications. Any other cleanup should be done here
</del><ins>+        Finish monitoring push notifications.
</ins><span class="cx">         &quot;&quot;&quot;
</span><del>-        self._unsubscribeFromAll()
</del><ins>+        pushkeys = self._ampPushkeys.keys()
+        self._ampPushkeys = {}
+        yield self._unsubscribeFromPushkeys(pushkeys)
</ins><span class="cx"> 
</span><del>-    def _subscribeToIDs(self, ids):
</del><ins>+        # Close the connection between client and server
+        yield self._protocol.transport.loseConnection()
+        self._connected = False
</ins><span class="cx"> 
</span><del>-        subscribeToIDs(
-            self._ampPushHost,
-            self._ampPushPort,
-            ids,
-            self._receivedAmpPush,
-            self._reactor
-        )
</del><span class="cx"> 
</span><del>-    def _receivedAMPPush(self, inboundID, dataChangedTimestamp, priority=5):
-        print(&quot;-&quot; * 64)
-        print(&quot;{} received a PUSH with ID={}, timestamp={}, priority={}&quot;.format(self.record.commonName, inboundID, dataChangedTimestamp, priority))
-        print(&quot;By the way, my AMP keys are {}&quot;.format(self._ampPushkeys))
-        print(&quot;-&quot; * 64)
</del><ins>+    def addPushkey(self, pushkey, href):
+        &quot;&quot;&quot;
+        Register a pushkey associated with a specific calendar href.
</ins><span class="cx"> 
</span><del>-        for href, calendar_id in self.ampPushkeys.iteritems():
-            if inboundID == calendar_id:
-                self.callback(href)
-                break
-        else:
-            # Somehow we are not subscribed to this inboundID
-            print(&quot;*&quot; * 16 + &quot;Oh no - we're not subscribed to &quot; + str(inboundID) + &quot; but we received a notification anyway!&quot;)
-            pass
</del><ins>+        @param pushkey: AMP pushkey returned by the server, used to listen to notifications
+        @type pushkey: C{str}
+        @param href: href of calendar home set. When the server triggers a push for the
+            associated pushkey, the callback will be fired with this href
+        @type href: C{str}
</ins><span class="cx"> 
</span><del>-    def _unsubscribeFromAll(self):
-        # For now, the server doesn't support unsubscribing from pushkeys, so we simply
-        # &quot;forget&quot; about our registered pushkeys
-        self._ampPushkeys = {}
</del><ins>+        Example Usage:
+            monitor.addPushkey('/CalDAV/localhost/&lt;uid&gt;', '/calendars/__uids__/&lt;uid&gt;')
+        &quot;&quot;&quot;
+        self._ampPushkeys[pushkey] = href
+        if self._connected:
+            return self._subscribeToPushkey(pushkey)
</ins><span class="cx"> 
</span><ins>+    def removePushkey(self, pushkey):
+        &quot;&quot;&quot;
+        Unregister the calendar home associated with the specified pushkey
+        &quot;&quot;&quot;
+        if pushkey in self._ampPushkeys:
+            del self._ampPushkeys[pushkey]
+        if self._connected:
+            return self._unsubscribeFromPushkey(pushkey)
</ins><span class="cx"> 
</span><del>-    def addPushkey(self, href, pushkey):
-        self._ampPushkeys[href] = pushkey
-        self.subscribeToIDs()
</del><ins>+    def isSubscribedTo(self, href):
+        &quot;&quot;&quot;
+        Returns true if and only if the given calendar href is actively being monitored
+        &quot;&quot;&quot;
+        return href in self._ampPushkeys.itervalues()
</ins><span class="cx"> 
</span><ins>+    @inlineCallbacks
+    def _subscribeToPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._subscribeToPushkey(pushkey)
</ins><span class="cx"> 
</span><del>-    def removePushkey(self, pushkey):
-        # if self.ampPushkeys.has_value(pushkey):
-        #     del self.ampPushKeys
-        pass
</del><ins>+    @inlineCallbacks
+    def _unsubscribeFromPushkeys(self, pushkeys):
+        for pushkey in pushkeys:
+            yield self._unsubscribeFromPushkey(pushkey)
</ins><span class="cx"> 
</span><del>-    def isSubscribedTo(self, href):
-        return href in self.ampPushkeys
</del><ins>+    def _subscribeToPushkey(self, pushkey):
+        return self._protocol.callRemote(SubscribeToID, token=self._token, id=pushkey)
+
+    def _unsubscribeFromPushkey(self, pushkey):
+        return self._protocol.callRemote(UnsubscribeFromID, id=pushkey)
+
+
+    def _receivedAMPPush(self, inboundID, dataChangedTimestamp, priority=5):
+        if inboundID in self._ampPushkeys:
+            # Only react if we're tracking this pushkey
+            href = self._ampPushkeys[inboundID]
+            self._callback(href)
+        else:
+            # Somehow we are not subscribed to this pushkey
+            pass
</ins></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestrecordspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/records.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -11,6 +11,19 @@
</span><span class="cx">     def __repr__(self):
</span><span class="cx">         return &quot;Record(%s:%s %s %s %s)&quot; % (self.uid, self.password, self.commonName, self.email, self.guid)
</span><span class="cx"> 
</span><ins>+# def generateRecords(
+#     count, uidPattern=&quot;user%d&quot;, passwordPattern=&quot;user%d&quot;,
+#     namePattern=&quot;User %d&quot;, emailPattern=&quot;user%d@example.com&quot;,
+#     guidPattern=&quot;user%d&quot;
+# ):
+#     for i in xrange(count):
+#         i += 1
+#         uid = uidPattern % (i,)
+#         password = passwordPattern % (i,)
+#         name = namePattern % (i,)
+#         email = emailPattern % (i,)
+#         guid = guidPattern % (i,)
+#         yield DirectoryRecord(uid, password, name, email, guid)
</ins><span class="cx"> 
</span><span class="cx"> def recordsFromCSVFile(path):
</span><span class="cx">     if path:
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestrequesterpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/requester.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -24,7 +24,7 @@
</span><span class="cx"> class Requester(object):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Utility to create requests on behalf of a client. Public methods are:
</span><del>-    method     url     body     headers     status     method_label 
</del><ins>+    method     url     body     headers     status     method_label
</ins><span class="cx">     ------------------------------------------------------------------------
</span><span class="cx">     GET        req     ---
</span><span class="cx">     POST       req     req
</span><span class="lines">@@ -100,6 +100,7 @@
</span><span class="cx">             client_id=self._client_id,
</span><span class="cx">         )
</span><span class="cx"> 
</span><ins>+
</ins><span class="cx">         before = self._reactor.seconds()
</span><span class="cx">         response = yield self._agent.request(method, url, headers, StringProducer(body) if body else None)
</span><span class="cx"> 
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestresourcespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/resources.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -114,13 +114,6 @@
</span><span class="cx">         self.url = url
</span><span class="cx">         self.changeToken = changeToken
</span><span class="cx">         self.events = {}
</span><del>-        # print(&quot;----\nNew Calendar&quot;)
-        # print(&quot;Resource Type: &quot;, self.resourceType)
-        # print(&quot;Component Types: &quot;, self.componentTypes)
-        # print(&quot;Name: &quot;, self.name)
-        # print(&quot;URL: &quot;, self.url)
-        # print(&quot;Change Token: &quot;, self.changeToken)
-        # print(&quot;Events: &quot;, self.events)
</del><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def serialize(self):
</span><span class="lines">@@ -156,20 +149,7 @@
</span><span class="cx">                 calendar.changeToken = &quot;&quot;
</span><span class="cx">         return calendar
</span><span class="cx"> 
</span><del>-    @staticmethod
-    def buildCalendarXML(order=0, component_type='VEVENT', rgba_color='FB524FFF', name='Simple Calendar'):
-        # TODO add timezone information
</del><span class="cx"> 
</span><del>-        # MakeCalendar(None, '/', name, )
-
-        # body = _STARTUP_CREATE_CALENDAR.format(
-        #     order=order,
-        #     component_type=component_type,
-        #     color=rgba_color,
-        #     name=name)
-        # return body
-        return &quot;&quot;
-
</del><span class="cx">     @staticmethod
</span><span class="cx">     def addInviteeXML(uid, summary, readwrite=True):
</span><span class="cx">         return AddInvitees(None, '/', [uid], readwrite, summary=summary).request_data.text
</span><span class="lines">@@ -231,11 +211,3 @@
</span><span class="cx">         el = ElementTree.Element(qn)
</span><span class="cx">         el.text = order
</span><span class="cx">         return Calendar._buildPropPatchXML(el)
</span><del>-
-    # @inlineCallbacks
-    # def setCalendarProperty(self, calendar):
-
-    # def do_stuff(...):
-    #     body = 
-    #     yield self.requester.proppatch(href, body, method_label=&quot;PROPPATCH{calendar}&quot;)
-    #     
</del><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsaltsettingsplistclientsplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/alt-settings/plist/clients.plist        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -38,7 +38,7 @@
</span><span class="cx">                                 &lt;dict&gt;
</span><span class="cx">                                         &lt;!-- Name that appears in logs. --&gt;
</span><span class="cx">                                         &lt;key&gt;title&lt;/key&gt;
</span><del>-                                        &lt;string&gt;10.11 Intern&lt;/string&gt;
</del><ins>+                                        &lt;string&gt;10.11&lt;/string&gt;
</ins><span class="cx"> 
</span><span class="cx">                                         &lt;!-- OS_X_10_7 can poll the calendar home at some interval. This is
</span><span class="cx">                                                 in seconds. --&gt;
</span><span class="lines">@@ -64,7 +64,7 @@
</span><span class="cx">                                 &lt;array&gt;
</span><span class="cx">                                         &lt;dict&gt;
</span><span class="cx">                                                 &lt;key&gt;class&lt;/key&gt;
</span><del>-                                                &lt;string&gt;contrib.performance.loadtest.profiles.Inviter&lt;/string&gt;
</del><ins>+                                                &lt;string&gt;contrib.performance.loadtest.profiles.Eventer&lt;/string&gt;
</ins><span class="cx"> 
</span><span class="cx">                                                 &lt;key&gt;params&lt;/key&gt;
</span><span class="cx">                                                 &lt;dict&gt;
</span><span class="lines">@@ -74,7 +74,7 @@
</span><span class="cx">                                                         &lt;!-- Define the interval (in seconds) at which this profile will use
</span><span class="cx">                                                                 its client to create a new event. --&gt;
</span><span class="cx">                                                         &lt;key&gt;interval&lt;/key&gt;
</span><del>-                                                        &lt;integer&gt;1&lt;/integer&gt;
</del><ins>+                                                        &lt;integer&gt;2&lt;/integer&gt;
</ins><span class="cx">                                                 &lt;/dict&gt;
</span><span class="cx">                                         &lt;/dict&gt;
</span><span class="cx">                                 &lt;/array&gt;
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsclientspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/clients.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,35 +1,26 @@
</span><span class="cx"> from contrib.performance.loadtest.clients import iOS_5, OS_X_10_6, OS_X_10_7, OS_X_10_11
</span><del>-from contrib.performance.loadtest.profiles import CalendarMaker, CalendarUpdater, CalendarSharer, CalendarDeleter
-from contrib.performance.loadtest.population import ProfileType
</del><ins>+from contrib.performance.loadtest.profiles import (
+    Eventer, EventDeleter,
+    Titler,
+    Tasker, TaskDeleter,
+    TaskTitler, TaskNoter, Completer, Prioritizer,
</ins><span class="cx"> 
</span><del>-from preset_distributions import STANDARD_WORK_DISTRIBUTION, LOW_RECURRENCE_DISTRIBUTION, MEDIUM_RECURRENCE_DISTRIBUTION
</del><ins>+    CalendarMaker, CalendarUpdater, CalendarSharer, CalendarDeleter
+)
+from contrib.performance.loadtest.distributions import FixedDistribution, BernoulliDistribution
</ins><span class="cx"> 
</span><del>-# We have to roll our own deep copy method because you can't deep copy Twisted's reactor
-class ClientFactory(object):
</del><span class="cx"> 
</span><del>-    def __init__(self, client, weight):
-        pass
</del><ins>+from preset_distributions import STANDARD_WORK_DISTRIBUTION, LOW_RECURRENCE_DISTRIBUTION, MEDIUM_RECURRENCE_DISTRIBUTION
</ins><span class="cx"> 
</span><del>-    @staticmethod
-    def _duplicateClient(client):
-        return type(client)(
-            # some params
-        )
-
-    def new(reactor, ):
-        pass
-
-class ProfileFactory(object):
-    def __init__(self, profile):
-        pass
-
-    @staticmethod
-    def _duplicateProfile(profile):
-        return type(profile)()
-
-calendars_only = [
</del><ins>+config = [
</ins><span class="cx">     {
</span><span class="cx">         &quot;software&quot;: OS_X_10_11,
</span><ins>+        #     title=&quot;10.11&quot;,
+        #     calendarHomePollInterval=5,
+        #     supportAmpPush=True,
+        #     ampPushHost=&quot;localhost&quot;,
+        #     ampPushPort62311
+        # )
</ins><span class="cx">         &quot;params&quot;: {
</span><span class="cx">             &quot;title&quot;: &quot;10.11&quot;,
</span><span class="cx">             &quot;calendarHomePollInterval&quot;: 5,
</span><span class="lines">@@ -38,33 +29,41 @@
</span><span class="cx">             &quot;ampPushPort&quot;: 62311
</span><span class="cx">         },
</span><span class="cx">         &quot;profiles&quot;: [
</span><del>-            ProfileType(CalendarMaker, dict(enabled=True, interval=15)),
</del><ins>+            # Eventer(enabled=True, interval=0.01, eventStartDistribution=STANDARD_WORK_DISTRIBUTION),
+            # Titler(enabled=True, interval=1, titleLengthDistribution=FixedDistribution(10)),
</ins><span class="cx"> 
</span><del>-            # CalendarMaker(enabled=True, interval=15),
</del><ins>+            # Tasker(enabled=False, interval=1),
+            # Completer(enabled=True, interval=0.5, completeLikelihood=BernoulliDistribution(0.5)),
+            # Prioritizer(enabled=True, interval=0.1),
+            TaskTitler(enabled=True, interval=1),
+            TaskNoter(enabled=True, interval=1),
+            # TaskDeleter(enabled=True, interval=1),
+
+            # CalendarMaker(enabled=True, interval=1),
</ins><span class="cx">             # CalendarUpdater(enabled=True, interval=5),
</span><span class="cx">             # CalendarSharer(enabled=True, interval=30),
</span><span class="cx">             # CalendarDeleter(false=True, interval=30)
</span><span class="cx">         ],
</span><del>-        &quot;weight&quot;: 1
</del><ins>+        &quot;weight&quot;: 3
</ins><span class="cx">     }
</span><span class="cx"> ]
</span><span class="cx"> 
</span><del>-# TBD what about multiple weights?
-calendars_only_ideal = [
-    OS_X_10_11(
-        title=&quot;10.11&quot;,
-        calendarHomePollInterval=5,
-        supportAmpPush=True,
-        ampPushHost=&quot;localhost&quot;,
-        ampPushPort=62311,
-        profiles=[
-            CalendarMaker(enabled=True, interval=15),
-            # CalendarUpdater(enabled=True, interval=5),
-            # CalendarSharer(enabled=False, interval=30),
-            # CalendarDeleter(enabled=False, interval=30)
-        ]
-    )
-]
</del><ins>+# # TBD what about multiple weights?
+# calendars_only_ideal = [
+#     OS_X_10_11(
+#         title=&quot;10.11&quot;,
+#         calendarHomePollInterval=5,
+#         supportAmpPush=True,
+#         ampPushHost=&quot;localhost&quot;,
+#         ampPushPort=62311,
+#         profiles=[
+#             CalendarMaker(enabled=True, interval=15),
+#             # CalendarUpdater(enabled=True, interval=5),
+#             # CalendarSharer(enabled=False, interval=30),
+#             # CalendarDeleter(enabled=False, interval=30)
+#         ]
+#     )
+# ]
</ins><span class="cx"> 
</span><span class="cx"> # event_updates_only = [
</span><span class="cx"> #     {
</span><span class="lines">@@ -111,7 +110,3 @@
</span><span class="cx"> #         &quot;weight&quot;: 1
</span><span class="cx"> #     }
</span><span class="cx"> # ]
</span><del>-
-
-# clientConfiguration = calendars_only
-# __all__ = [clientConfiguration]
</del></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsettingsconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/settings/config.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,22 +1,13 @@
</span><del>-# Generally, the defaults are good enough for us.
</del><ins>+from contrib.performance.loadtest.settings.defaults import arrival, requestLogger, operationLogger, statisticsReporter, accounts
</ins><span class="cx"> 
</span><del>-config = dict(
-    server=server,
-    webadminPort=8080,
-    serverStatsPort=8100,
-    serializationPath='/tmp/sim',
-    arrival=arrival,
-    observers=[_requestLogger, _operationLogger, _statisticsReporter],
-    records=accounts
-)
</del><ins>+from contrib.performance.loadtest.logger import EverythingLogger, MessageLogger
</ins><span class="cx"> 
</span><del>-config_dist = dict(
-    server=server,
-    webadminPort=8080,
-    serverStatsPort=8100,
-    serializationPath='/tmp/sim',
-    arrival=arrival,
-    observers=[_requestLogger, _operationLogger, _statisticsReporter],
-    records=accounts,
-    workers=[&quot;./bin/python contrib/performance/loadtest/ampsim.py&quot;] * 6,
-)
</del><span class="cx">\ No newline at end of file
</span><ins>+config = {
+    &quot;server&quot;: 'https://127.0.0.1:8443',
+    &quot;webadminPort&quot;: 8080,
+    &quot;serverStatsPort&quot;: {'server': 'localhost', 'Port': 8100},
+    &quot;serializationPath&quot;: '/tmp/sim',
+    &quot;arrival&quot;: arrival,
+    &quot;observers&quot;: [requestLogger, operationLogger, statisticsReporter, EverythingLogger(), MessageLogger()],
+    &quot;records&quot;: accounts
+}
</ins></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestsimpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/sim.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -48,8 +48,9 @@
</span><span class="cx"> from contrib.performance.loadtest.clients import OS_X_10_6
</span><span class="cx"> from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
</span><span class="cx"> from contrib.performance.loadtest.population import (
</span><del>-    Populator, ProfileType, ClientType, PopulationParameters, SmoothRampUp,
</del><ins>+    ClientFactory, PopulationParameters, SmoothRampUp,
</ins><span class="cx">     CalendarClientSimulator)
</span><ins>+from contrib.performance.loadtest.config import Config
</ins><span class="cx"> from contrib.performance.loadtest.webadmin import LoadSimAdminResource
</span><span class="cx"> 
</span><span class="cx"> def safeDivision(value, total, factor=1):
</span><span class="lines">@@ -88,19 +89,30 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     config = None
</span><span class="cx">     settings = FilePath(__file__).sibling(&quot;settings&quot;)
</span><del>-    _defaultConfig = settings.child(&quot;config.plist&quot;)
-    _defaultClients = settings.child(&quot;clients.plist&quot;)
</del><ins>+    # plists = settings.child('alt-settings').child('plist')
+    # _defaultConfig = plists.child(&quot;config.plist&quot;)
+    # _defaultClients = plists.child(&quot;clients.plist&quot;)
+    # _defaultConfig = settings.child('settings.config')
+    # _defaultClients = settings.child('settings.clients')
+    _defaultConfig = 'contrib.performance.loadtest.settings.config'
+    _defaultClients = 'contrib.performance.loadtest.settings.clients'
</ins><span class="cx"> 
</span><ins>+    optFlags = [
+        (&quot;debug&quot;, &quot;d&quot;, &quot;Enable Deferred and Failure debugging&quot;),
+        (&quot;debug-deferred&quot;, None, &quot;Enable only Deferred debugging&quot;),
+        (&quot;debug-failure&quot;, None, &quot;Enable only Failure debugging&quot;),
+        (&quot;use-plist&quot;, None, &quot;Interpret configuration files as XML property lists (default false)&quot;)
+    ]
+
</ins><span class="cx">     optParameters = [
</span><span class="cx">         (&quot;runtime&quot;, &quot;t&quot;, None,
</span><span class="cx">          &quot;Specify the limit (seconds) on the time to run the simulation.&quot;,
</span><span class="cx">          int),
</span><span class="cx">         (&quot;config&quot;, None, _defaultConfig,
</span><del>-         &quot;Configuration plist file name from which to read simulation parameters.&quot;,
-         FilePath),
</del><ins>+         &quot;Configuration plist file name from which to read simulation parameters.&quot;),
</ins><span class="cx">         (&quot;clients&quot;, None, _defaultClients,
</span><del>-         &quot;Configuration plist file name from which to read client parameters.&quot;,
-         FilePath),
</del><ins>+         &quot;Configuration plist file name from which to read client parameters.&quot;),
+        (&quot;logfile&quot;, &quot;l&quot;, '-', FilePath)
</ins><span class="cx">     ]
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -140,40 +152,48 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def postOptions(self):
</span><del>-        try:
-            configFile = self['config'].open()
-        except IOError, e:
-            raise UsageError(&quot;--config %s: %s&quot; % (
-                self['config'].path, e.strerror))
-        try:
</del><ins>+        &quot;&quot;&quot;
+        Convert the given configuration files to dictionaries, respectively in
+        self.config and self.clients
+        &quot;&quot;&quot;
+        configPath = self[&quot;config&quot;]
+        clientsPath = self[&quot;clients&quot;]
+        if self['use-plist']:
</ins><span class="cx">             try:
</span><del>-                self.config = readPlist(configFile)
</del><ins>+                with open(configPath) as configFile: # Could raise an IOError
+                    self.config = readPlist(configFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError(&quot;--config %s: %s&quot; % (configPath.path, e.strerror))
</ins><span class="cx">             except ExpatError, e:
</span><del>-                raise UsageError(&quot;--config %s: %s&quot; % (self['config'].path, e))
-        finally:
-            configFile.close()
</del><ins>+                raise UsageError(&quot;--config %s: %s&quot; % (configPath.path, e))
</ins><span class="cx"> 
</span><del>-        try:
-            # from importlib import import_module
-            # client_config = import_module(&quot;contrib.performance.loadtest.settings.clients&quot;)
-            clientFile = self['clients'].open()
-        except IOError, e:
-            raise UsageError(&quot;--clients %s: %s&quot; % (
-                self['clients'].path, e.strerror))
-        try:
</del><span class="cx">             try:
</span><del>-                client_config = readPlist(clientFile)
-                # self.config[&quot;clients&quot;] = client_config.calendars_only# client_config[&quot;clients&quot;]
-                self.config[&quot;clients&quot;] = client_config[&quot;clients&quot;]
-                if &quot;arrivalInterval&quot; in client_config:
-                    self.config[&quot;arrival&quot;][&quot;params&quot;][&quot;interval&quot;] = client_config[&quot;arrivalInterval&quot;]
</del><ins>+                with open(clientsPath) as clientFile: # Could raise an IOError
+                    self.clients = readPlist(clientFile) # Could raise an ExpatError
+            except IOError, e:
+                raise UsageError(&quot;--clients %s: %s&quot; % (clientsPath.path, e.strerror))
</ins><span class="cx">             except ExpatError, e:
</span><del>-                raise UsageError(&quot;--clients %s: %s&quot; % (self['clients'].path, e))
-        finally:
-            # clientFile.close()
-            pass
</del><ins>+                raise UsageError(&quot;--clients %s: %s&quot; % (clientsPath.path, e))
</ins><span class="cx"> 
</span><ins>+            self.config[&quot;clients&quot;] = self.clients[&quot;clients&quot;]
</ins><span class="cx"> 
</span><ins>+        else:
+            from importlib import import_module
+            try:
+                config = import_module(configPath) # Could raise ImportError
+                self.config = config.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError(&quot;--config %s: %s&quot; % (configPath, e))
+            try:
+                clients = import_module(clientsPath) # Could raise ImportError
+                self.clients = clients.config # Could raise attribute error
+            except (ImportError, AttributeError), e:
+                raise UsageError(&quot;--clients %s: %s&quot; % (clientsPath, e))
+
+        self.configObj = Config()
+        self.configObj.populateFrom(self.config, self.clients)
+
+
</ins><span class="cx"> Arrival = namedtuple('Arrival', 'factory parameters')
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -194,6 +214,8 @@
</span><span class="cx">     def __init__(self, server, webadminPort, serverStats, serializationPath, arrival, parameters, observers=None,
</span><span class="cx">                  records=None, reactor=None, runtime=None, workers=None,
</span><span class="cx">                  configTemplate=None, workerID=None, workerCount=1):
</span><ins>+        if configTemplate == {}:
+            raise Exception('Got here!')
</ins><span class="cx">         if reactor is None:
</span><span class="cx">             from twisted.internet import reactor
</span><span class="cx">         self.server = server
</span><span class="lines">@@ -224,7 +246,7 @@
</span><span class="cx">         except UsageError, e:
</span><span class="cx">             raise SystemExit(str(e))
</span><span class="cx"> 
</span><del>-        return cls.fromConfig(options.config, options['runtime'], output)
</del><ins>+        return cls.fromConfigObject(options.configObj, options['runtime'], output)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="lines">@@ -338,7 +360,6 @@
</span><span class="cx">             observers=observers,
</span><span class="cx">             records=records,
</span><span class="cx">             runtime=runtime,
</span><del>-            reactor=reactor,
</del><span class="cx">             workers=workers,
</span><span class="cx">             configTemplate=configTemplate,
</span><span class="cx">             workerID=workerID,
</span><span class="lines">@@ -348,138 +369,47 @@
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><span class="cx">     def fromConfigObject(cls, config, runtime=None, output=stdout):
</span><del>-        workers = config['workers']
-        if workers is None:
-            # Client / place where the simulator actually runs configuration
-            workerID = config.get(&quot;workerID&quot;, 0)
-            workerCount = config.get(&quot;workerCount&quot;, 1)
-            configTemplate = None
-            server = config.get('server', 'http://127.0.0.1:8008')
-            serializationPath = None
</del><ins>+        # if config.isManaging:
+        #     observers = 
</ins><span class="cx"> 
</span><del>-            serializationPath = config['serializationPath']
</del><ins>+        # if config.isWorking:
+        #     simulator = CalendarClientSimulator(
+        #         config.records,
+        #         config.parameters,
+        #         config.reactor,
+        #         config.server,
+        #         config.serializationPath,
+        #         config.workerID,
+        #         config.workerCount,
+        #     )
+        #     arrival = config.arrival
</ins><span class="cx"> 
</span><del>-            if 'arrival' in config:
-                arrival = Arrival(
-                    namedAny(config['arrival']['factory']),
-                    config['arrival']['params'])
-            else:
-                arrival = Arrival(
-                    SmoothRampUp, dict(groups=10, groupSize=1, interval=3))
-
-            parameters = PopulationParameters()
-            if 'clients' in config:
-                for clientConfig in config['clients']:
-                    parameters.addClient(
-                        clientConfig[&quot;weight&quot;],
-                        ClientType(
-                            clientConfig[&quot;software&quot;],
-                            clientConfig[&quot;params&quot;],
-                            clientConfig[&quot;profiles&quot;]
-                        )
-                    )
-                        # ClientType(
-                        #     namedAny(clientConfig[&quot;software&quot;]),
-                        #     cls._convertParams(clientConfig[&quot;params&quot;]),
-                        #     [
-                        #         ProfileType(
-                        #             namedAny(profile[&quot;class&quot;]),
-                        #             cls._convertParams(profile[&quot;params&quot;])
-                        #         ) for profile in clientConfig[&quot;profiles&quot;]
-                        #     ]))
-            if not parameters.clients:
-                parameters.addClient(1,
-                                     ClientType(OS_X_10_6, {},
-                                                [Eventer, Inviter, Accepter]))
-        else:
-            # Manager / observer process.
-            server = ''
-            serializationPath = None
-            arrival = None
-            parameters = None
-            workerID = 0
-            configTemplate = config
-            workerCount = 1
-
-        # webadminPort = 
-        webadminPort = None
-        if 'webadmin' in config:
-            if config['webadmin']['enabled']:
-                webadminPort = config['webadmin']['HTTPPort']
-
-        serverStats = None
-        if 'serverStats' in config:
-            if config['serverStats']['enabled']:
-                serverStats = config['serverStats']
-                serverStats['server'] = config['server'] if 'server' in config else ''
-
-        observers = []
-        if 'observers' in config:
-            for observer in config['observers']:
-                observerName = observer[&quot;type&quot;]
-                observerParams = observer[&quot;params&quot;]
-                observers.append(namedAny(observerName)(**observerParams))
-
-        records = []
-        if 'accounts' in config:
-            loader = config['accounts']['loader']
-            params = config['accounts']['params']
-            records.extend(namedAny(loader)(**params))
-            output.write(&quot;Loaded {0} accounts.\n&quot;.format(len(records)))
-
</del><span class="cx">         return cls(
</span><del>-            server,
-            webadminPort,
-            serverStats,
-            serializationPath,
-            arrival,
-            parameters,
-            observers=observers,
-            records=records,
</del><ins>+            config.server,
+            config.webadminPort,
+            config.serverStats,
+            config.serializationPath,
+            config.arrival,
+            config.parameters,
+            observers=config.observers,
+            records=config.records,
</ins><span class="cx">             runtime=runtime,
</span><del>-            reactor=reactor,
-            workers=workers,
-            configTemplate=configTemplate,
-            workerID=workerID,
-            workerCount=workerCount,
</del><ins>+            configTemplate=config.serializeForWorker,
+            workers=config.workers,
+            workerID=config.workerID,
+            workerCount=config.workerCount,
</ins><span class="cx">         )
</span><span class="cx"> 
</span><del>-    @classmethod
-    def _convertParams(cls, params):
-        &quot;&quot;&quot;
-        Find parameter values which should be more structured than plistlib is
-        capable of constructing and replace them with the more structured form.
</del><span class="cx"> 
</span><del>-        Specifically, find keys that end with C{&quot;Distribution&quot;} and convert
-        them into some kind of distribution object using the associated
-        dictionary of keyword arguments.
-        &quot;&quot;&quot;
-        for k, v in params.iteritems():
-            if k.endswith('Distribution'):
-                params[k] = cls._convertDistribution(v)
-        return params
-
-
</del><span class="cx">     @classmethod
</span><del>-    def _convertDistribution(cls, value):
-        &quot;&quot;&quot;
-        Construct and return a new distribution object using the type and
-        params specified by C{value}.
-        &quot;&quot;&quot;
-        return namedAny(value['type'])(**value['params'])
-
-
-    @classmethod
</del><span class="cx">     def main(cls, args=None):
</span><span class="cx">         simulator = cls.fromCommandLine(args)
</span><span class="cx">         raise SystemExit(simulator.run())
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def createSimulator(self):
</span><del>-        populator = Populator(Random())
</del><span class="cx">         return CalendarClientSimulator(
</span><span class="cx">             self.records,
</span><del>-            populator,
</del><span class="cx">             self.parameters,
</span><span class="cx">             self.reactor,
</span><span class="cx">             self.server,
</span><span class="lines">@@ -491,7 +421,8 @@
</span><span class="cx"> 
</span><span class="cx">     def createArrivalPolicy(self):
</span><span class="cx">         # print(self.arrival.parameters)
</span><del>-        return self.arrival.factory(self.reactor, **self.arrival.parameters)
</del><ins>+        # return zory(self.reactor, **self.arrival.parameters)
+        return self.arrival
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     def serviceClasses(self):
</span><span class="lines">@@ -503,6 +434,7 @@
</span><span class="cx">             PrimaryService = WorkerSpawnerService
</span><span class="cx">         else:
</span><span class="cx">             PrimaryService = SimulatorService
</span><ins>+
</ins><span class="cx">         return [
</span><span class="cx">             ObserverService,
</span><span class="cx">             ReporterService,
</span><span class="lines">@@ -575,6 +507,7 @@
</span><span class="cx">             data = &quot;&quot;
</span><span class="cx">             while not data.endswith(&quot;\n&quot;):
</span><span class="cx">                 d = s.recv(1024)
</span><ins>+                print(&quot;Received data from stats socket: &quot;, d)
</ins><span class="cx">                 if d:
</span><span class="cx">                     data += d
</span><span class="cx">                 else:
</span><span class="lines">@@ -622,6 +555,9 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         super(ObserverService, self).startService()
</span><span class="cx">         for obs in self.loadsim.observers:
</span><ins>+            import random
+            val = random.random()
+            msg(type='log', text='Adding observer: ' + obs.__class__.__name__, val=str(val))
</ins><span class="cx">             addObserver(obs.observe)
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -639,16 +575,19 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">     def startService(self):
</span><ins>+        print(&quot;Starting simulator service&quot;)
</ins><span class="cx">         super(SimulatorService, self).startService()
</span><span class="cx">         self.clientsim = self.loadsim.createSimulator()
</span><span class="cx">         arrivalPolicy = self.loadsim.createArrivalPolicy()
</span><del>-        arrivalPolicy.run(self.clientsim)
</del><ins>+        arrivalPolicy.run(self.loadsim.reactor, self.clientsim)
+        # self.loadsim.arrival.run(self.loadsim.reactor, self.loadsim.simulator)
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx">     @inlineCallbacks
</span><span class="cx">     def stopService(self):
</span><span class="cx">         yield super(SimulatorService, self).stopService()
</span><span class="cx">         yield self.clientsim.stop()
</span><ins>+        # yield self.loadsim.simulator.stop()
</ins><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -662,6 +601,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Start observing.
</span><span class="cx">         &quot;&quot;&quot;
</span><ins>+        print(&quot;Starting reporter service&quot;)
</ins><span class="cx">         super(ReporterService, self).startService()
</span><span class="cx">         self.loadsim.reporter = self
</span><span class="cx"> 
</span><span class="lines">@@ -736,6 +676,7 @@
</span><span class="cx">         super(WorkerSpawnerService, self).startService()
</span><span class="cx">         self.bridges = []
</span><span class="cx">         for workerID, worker in enumerate(self.loadsim.workers):
</span><ins>+            print(&quot;Building bridge for #&quot; + str(workerID))
</ins><span class="cx">             bridge = ProcessProtocolBridge(
</span><span class="cx">                 self, Manager(self.loadsim, workerID, len(self.loadsim.workers),
</span><span class="cx">                               self.output)
</span></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_distributionspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_distributions.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,25 +1,91 @@
</span><span class="cx"> from twisted.trial.unittest import TestCase
</span><span class="cx"> 
</span><span class="cx"> from contrib.performance.loadtest.distributions import (
</span><del>-    LogNormalDistribution, UniformDiscreteDistribution,
-    UniformIntegerDistribution, WorkDistribution, RecurrenceDistribution
</del><ins>+    # Continuous distributions
+    LogNormalDistribution, NormalDistribution,
+    # Discrete distributions
+    UniformDiscreteDistribution, UniformIntegerDistribution,
+    BernoulliDistribution, BinomialDistribution, FixedDistribution,
+    # Calendar-specific distributions
+    WorkDistribution, RecurrenceDistribution,
</ins><span class="cx"> )
</span><span class="cx"> 
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><span class="cx"> from pycalendar.timezone import Timezone
</span><span class="cx"> 
</span><ins>+from scipy import stats
+from scipy.optimize import curve_fit
+import itertools
+
+&quot;&quot;&quot;
+Disclaimer: These tests are nondeterministic, so be careful
+&quot;&quot;&quot;
+
</ins><span class="cx"> class DistributionTestBase(TestCase):
</span><del>-    def getSamples(self, n):
</del><ins>+    def get_n_samples(self, dist, n):
</ins><span class="cx">         samples = []
</span><span class="cx">         for _ignore_i in xrange(n):
</span><del>-            samples.append()
</del><ins>+            samples.append(dist.sample())
+        return samples
</ins><span class="cx"> 
</span><del>-    def close(self, n):
-        pass
</del><ins>+class DiscreteDistributionTests(DistributionTestBase):
+    def test_bernoulli(self):
+        sample_count = 1000
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for prop in proportions:
+            dist = BernoulliDistribution(proportion=prop)
+            samples = self.get_n_samples(dist, sample_count)
+            successes = samples.count(True)
</ins><span class="cx"> 
</span><del>-class DistributionTests(TestCase):
</del><ins>+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value &lt;= 0.01, &quot;%d/%d, expected %f&quot; % (successes, sample_count, prop))
</ins><span class="cx"> 
</span><ins>+    def test_binomial(self):
+        sample_counts = [100, 1000, 10000]
+        proportions = [0, 0.1, 0.25, 0.5, 0.75, 0.9, 1]
+        for sample_count, prop in itertools.product(sample_counts, proportions):
+            dist = BinomialDistribution(p=prop, n=sample_count)
+            successes = dist.sample()
</ins><span class="cx"> 
</span><ins>+            # This representes the likelihood that we would see as many successes
+            # as we did given that the true proportion is prop
+            p_value = stats.binom_test(successes, n=sample_count, p=prop)
+            self.assertFalse(p_value &lt;= 0.01, &quot;%d/%d, expected %f&quot; % (successes, sample_count, prop))
+
+    def test_fixed(self):
+        dist = FixedDistribution(4) # https://xkcd.com/221/
+        for _ignore_i in xrange(100):
+            self.assertEqual(dist.sample(), 4)
+
+    def test_uniformdiscrete(self):
+        population = [82, 101, 100, 109, 111, 110, 100]
+        counts = dict.fromkeys(population, 0)
+        dist = UniformDiscreteDistribution(population)
+        for _ignore_i in range(len(population) * 10):
+            counts[dist.sample()] += 1
+        self.assertEqual(dict.fromkeys(population, 10), counts)
+        # Do some chi squared stuff
+
+    def test_uniform(self):
+        dist = UniformIntegerDistribution(-5, 10)
+        for _ignore_i in range(100):
+            value = dist.sample()
+            self.assertTrue(-5 &lt;= value &lt; 10)
+            self.assertIsInstance(value, int)
+
+class ContinuousDistributionTests(TestCase):
+    def is_fit(self, pdf, xdata, ydata, pexp):
+        &quot;&quot;&quot;
+        expected parameters
+        &quot;&quot;&quot;
+        popt, pcov = curve_fit(pdf, xdata, ydata)
+        print popt
+
+    def test_normal(self):
+        dist = NormalDistribution()
+
</ins><span class="cx">     def test_lognormal(self):
</span><span class="cx">         dist = LogNormalDistribution(mu=1, sigma=1)
</span><span class="cx">         for _ignore_i in range(100):
</span><span class="lines">@@ -48,16 +114,8 @@
</span><span class="cx">         self.assertRaises(ValueError, LogNormalDistribution, mean=1)
</span><span class="cx">         self.assertRaises(ValueError, LogNormalDistribution, median=1)
</span><span class="cx"> 
</span><ins>+class CalendarDistributionTests(TestCase):
</ins><span class="cx"> 
</span><del>-    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)
-
-
</del><span class="cx">     def test_workdistribution(self):
</span><span class="cx">         tzname = &quot;US/Eastern&quot;
</span><span class="cx">         dist = WorkDistribution([&quot;mon&quot;, &quot;wed&quot;, &quot;thu&quot;, &quot;sat&quot;], 10, 20, tzname)
</span><span class="lines">@@ -91,7 +149,7 @@
</span><span class="cx">             self.assertTrue(value is None)
</span><span class="cx"> 
</span><span class="cx">         dist = RecurrenceDistribution(True, {&quot;daily&quot;: 1, &quot;none&quot;: 2, &quot;weekly&quot;: 1})
</span><del>-        dist._helperDistribution = UniformDiscreteDistribution([0, 3, 2, 1, 0], randomize=False)
</del><ins>+        dist._helperDistribution = UniformDiscreteDistribution([0, 3, 2, 1, 0])
</ins><span class="cx">         value = dist.sample()
</span><span class="cx">         self.assertTrue(value is not None)
</span><span class="cx">         value = dist.sample()
</span><span class="lines">@@ -102,11 +160,3 @@
</span><span class="cx">         self.assertTrue(value is not None)
</span><span class="cx">         value = dist.sample()
</span><span class="cx">         self.assertTrue(value is not None)
</span><del>-
-
-    def test_uniform(self):
-        dist = UniformIntegerDistribution(-5, 10)
-        for _ignore_i in range(100):
-            value = dist.sample()
-            self.assertTrue(-5 &lt;= value &lt; 10)
-            self.assertIsInstance(value, int)
</del></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_pushpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_push.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -1,15 +1,19 @@
</span><span class="cx"> from twisted.trial.unittest import TestCase
</span><ins>+from twisted.internet.defer import inlineCallbacks
</ins><span class="cx"> 
</span><del>-from calendarserver.push.amppush import AMPPushMaster
-
</del><span class="cx"> from contrib.performance.loadtest.push import PushMonitor
</span><span class="cx"> 
</span><span class="cx"> class PushMonitorTests(TestCase):
</span><del>-    def fakePush(self, inboundID, dataChangedTimestamp, priority=5):
-        self.monitor._receivedAMPPush(inboundID, dataChangedTimestamp, priority)
</del><ins>+    def sendFakePush(self, pushkey):
+        self.monitor._receivedAMPPush(inboundID=pushkey, dataChangedTimestamp=None, priority=None)
</ins><span class="cx"> 
</span><ins>+    def receivedPush(self, calendar_href):
+        self.history.append(calendar_href)
+
</ins><span class="cx">     def setUp(self):
</span><del>-        self.pushMaster = AMPPushMaster()
</del><ins>+        &quot;&quot;&quot;
+        Creates and begins a PushMonitor with a history-tracking callback
+        &quot;&quot;&quot;
</ins><span class="cx"> 
</span><span class="cx">         self.monitor = PushMonitor(
</span><span class="cx">             None,
</span><span class="lines">@@ -17,16 +21,94 @@
</span><span class="cx">             62311,
</span><span class="cx">             self.receivedPush
</span><span class="cx">         )
</span><del>-        self.monitor.begin()
</del><ins>+        self.history = []
+        return self.monitor.begin()
</ins><span class="cx"> 
</span><del>-    def sendNotification(self, href, pushkey):
-        self.pushMaster.notify(href, pushkey, None, None)
</del><ins>+    def test_noPushkey(self):
+        &quot;&quot;&quot;
+        Monitor will not react to a push if there are no registered pushkeys
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
</ins><span class="cx"> 
</span><ins>+    @inlineCallbacks
</ins><span class="cx">     def test_addPushkey(self):
</span><del>-        pass
</del><ins>+        &quot;&quot;&quot;
+        Adding a pushkey triggers a notification for the corresponding calendar home
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
</ins><span class="cx"> 
</span><ins>+    @inlineCallbacks
</ins><span class="cx">     def test_removePushkey(self):
</span><del>-        pass
</del><ins>+        &quot;&quot;&quot;
+        Pushkeys can be unregistered
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.removePushkey(pushkey)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
</ins><span class="cx"> 
</span><ins>+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [])
+
+    @inlineCallbacks
+    def test_addDuplicatePushkeys(self):
+        &quot;&quot;&quot;
+        Adding the same pushkey twice only registers it once
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home])
+
+    @inlineCallbacks
+    def test_addOverridePushkeys(self):
+        &quot;&quot;&quot;
+        Adding the same pushkey with a different calendar home
+        unregisters the original
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey, calendar_home_2)
+        self.assertFalse(self.monitor.isSubscribedTo(calendar_home))
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2])
+
+    @inlineCallbacks
+    def test_multiplePushkeys(self):
+        &quot;&quot;&quot;
+        Monitor supports registering multiple pushkeys
+        &quot;&quot;&quot;
+        pushkey = '/CalDAV/abc/def/'
+        pushkey_2 = '/CalDAV/abc/xyz/'
+        calendar_home = '/foo/bar/'
+        calendar_home_2 = '/foo/baz/'
+        yield self.monitor.addPushkey(pushkey, calendar_home)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home))
+        yield self.monitor.addPushkey(pushkey_2, calendar_home_2)
+        self.assertTrue(self.monitor.isSubscribedTo(calendar_home_2))
+        self.sendFakePush(pushkey_2)
+        self.assertEqual(self.history, [calendar_home_2])
+        self.sendFakePush(pushkey)
+        self.assertEqual(self.history, [calendar_home_2, calendar_home])
+
</ins><span class="cx">     def tearDown(self):
</span><del>-        self.monitor.end()
</del><ins>+        return self.monitor.end()
</ins></span></pre></div>
<a id="CalendarServerbranchesuserssredmondclientsimcontribperformanceloadtestteststest_resourcespy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py (15080 => 15081)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py        2015-08-31 20:06:29 UTC (rev 15080)
+++ CalendarServer/branches/users/sredmond/clientsim/contrib/performance/loadtest/tests/test_resources.py        2015-08-31 20:37:45 UTC (rev 15081)
</span><span class="lines">@@ -37,4 +37,3 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         event = Event(None, u'/bar/baz', u'etag')
</span><span class="cx">         self.assertIdentical(event.getUID(), None)
</span><del>-
</del></span></pre>
</div>
</div>

</body>
</html>