[CalendarServer-changes] [6821] CalendarServer/trunk/contrib/performance/loadtest

source_changes at macosforge.org source_changes at macosforge.org
Fri Jan 28 14:27:21 PST 2011


Revision: 6821
          http://trac.macosforge.org/projects/calendarserver/changeset/6821
Author:   exarkun at twistedmatrix.com
Date:     2011-01-28 14:27:21 -0800 (Fri, 28 Jan 2011)
Log Message:
-----------
Add a profile for accepting invitations; fix some problems with the invitation profile.  Lots of tests, too.

Modified Paths:
--------------
    CalendarServer/trunk/contrib/performance/loadtest/profiles.py

Added Paths:
-----------
    CalendarServer/trunk/contrib/performance/loadtest/test_profiles.py

Modified: CalendarServer/trunk/contrib/performance/loadtest/profiles.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/profiles.py	2011-01-28 22:26:27 UTC (rev 6820)
+++ CalendarServer/trunk/contrib/performance/loadtest/profiles.py	2011-01-28 22:27:21 UTC (rev 6821)
@@ -19,11 +19,13 @@
 Implementation of specific end-user behaviors.
 """
 
-from random import choice, gauss
+import random
 
 from vobject.base import ContentLine
 from vobject.icalendar import VEvent
 
+from protocol.caldav.definitions import caldavxml
+
 from twisted.internet.defer import succeed
 from twisted.internet.task import LoopingCall
 
@@ -32,6 +34,8 @@
     """
     A Calendar user who invites and de-invites other users to events.
     """
+    random = random
+
     def __init__(self, reactor, client, userNumber):
         self._reactor = reactor
         self._client = client
@@ -41,21 +45,26 @@
     def run(self):
         self._call = LoopingCall(self._invite)
         self._call.clock = self._reactor
+        # XXX Base this on something real
         self._call.start(3)
 
 
     def _addAttendee(self, event, attendees):
         """
-        Create a new attendeed to add to the list of attendees for the
+        Create a new attendee to add to the list of attendees for the
         given event.
         """
-        invitee = max(1, int(gauss(self._number, 30)))
-        if invitee == self._number:
-            # This will bias the distribution a little... it is the
-            # end of the world, probably.
-            invitee += 1
+        invitees = set([self._client.email[len('mailto:'):]])
+        for att in attendees:
+            invitees.add(att.params[u'EMAIL'][0])
+
+        while True:
+            invitee = max(1, int(self.random.gauss(self._number, 30)))
+            email = u'user%02d at example.com' % (invitee,)
+            if email not in invitees:
+                break
+
         user = u'User %02d' % (invitee,)
-        email = u'user%02d at example.com' % (invitee,)
         uuid = u'urn:uuid:user%02d' % (invitee,)
 
         attendee = ContentLine(
@@ -83,23 +92,85 @@
             otherwise a L{Deferred} which fires when the attendee
             change has been made.
         """
-        # Pick an event at random
-        if self._client._events:
-            uuid = choice(self._client._events.keys())
-            event = self._client._events[uuid].vevent
+        # Find calendars which are eligible for invites
+        calendars = [
+            cal 
+            for cal 
+            in self._client._calendars.itervalues() 
+            if cal.resourceType == caldavxml.calendar]
 
+        while calendars:
+            # Pick one at random from which to try to select an event
+            # to modify.
+            calendar = self.random.choice(calendars)
+            calendars.remove(calendar)
+
+            if not calendar.events:
+                continue
+
+            uuid = self.random.choice(calendar.events.keys())
+            event = calendar.events[uuid].vevent
+            href = calendar.url + uuid
+
             # Find out who might attend
             attendees = event.contents['vevent'][0].contents.get('attendee', [])
 
-            if len(attendees) < 2 or choice([False, True]):
-                d = self._addAttendee(event, attendees)
-                d.addCallback(
-                    lambda attendee:
-                        self._client.addEventAttendee(
-                            uuid, attendee))
-                return d
-            else:
-                # XXX
-                # self._removeAttendee(attendees)
-                return
+            d = self._addAttendee(event, attendees)
+            d.addCallback(
+                lambda attendee:
+                    self._client.addEventAttendee(
+                        href, attendee))
+            return d
 
+
+
+class Accepter(object):
+    """
+    A Calendar user who accepts invitations to events.
+    """
+    random = random
+
+    def __init__(self, reactor, client, userNumber):
+        self._reactor = reactor
+        self._client = client
+        self._number = userNumber
+
+
+    def run(self):
+        self._subscription = self._client.catalog["eventChanged"].subscribe(self.eventChanged)
+
+
+    def eventChanged(self, href):
+        # Just respond to normal calendar events
+        calendar = href.rsplit('/', 1)[0] + '/'
+        try:
+            calendar = self._client._calendars[calendar]
+        except KeyError:
+            return
+        if calendar.resourceType != caldavxml.calendar:
+            return
+
+        vevent = self._client._events[href].vevent
+        # Check to see if this user is in the attendee list in the
+        # NEEDS-ACTION PARTSTAT.
+        attendees = vevent.contents['vevent'][0].contents.get('attendee', [])
+        for attendee in attendees:
+            if attendee.params[u'EMAIL'][0] == self._client.email[len('mailto:'):]:
+                if attendee.params[u'PARTSTAT'][0] == 'NEEDS-ACTION':
+                    # XXX Base this on something real
+                    delay = 3 # self.random.gauss(10, 10)
+                    self._reactor.callLater(
+                        delay, self._acceptInvitation, href, attendee)
+                    return
+
+
+    def _acceptInvitation(self, href, attendee):
+        accepted = self._makeAcceptedAttendee(attendee)
+        self._client.changeEventAttendee(href, attendee, accepted)
+
+
+    def _makeAcceptedAttendee(self, attendee):
+        accepted = ContentLine.duplicate(attendee)
+        accepted.params[u'PARTSTAT'] = [u'ACCEPTED']
+        del accepted.params[u'RSVP']
+        return accepted

Added: CalendarServer/trunk/contrib/performance/loadtest/test_profiles.py
===================================================================
--- CalendarServer/trunk/contrib/performance/loadtest/test_profiles.py	                        (rev 0)
+++ CalendarServer/trunk/contrib/performance/loadtest/test_profiles.py	2011-01-28 22:27:21 UTC (rev 6821)
@@ -0,0 +1,397 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+"""
+Tests for loadtest.profiles.
+"""
+
+from vobject import readComponents
+
+from protocol.caldav.definitions import caldavxml
+
+from twisted.trial.unittest import TestCase
+from twisted.internet.task import Clock
+
+from loadtest.profiles import Inviter, Accepter
+from loadtest.ical import Calendar, Event
+
+SIMPLE_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:DAYLIGHT
+TZOFFSETFROM:-0500
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+DTSTART:20070311T020000
+TZNAME:EDT
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+DTSTART:20071104T020000
+TZNAME:EST
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20101018T155431Z
+UID:C98AD237-55AD-4F7D-9009-0D355D835822
+DTEND;TZID=America/New_York:20101021T130000
+TRANSP:OPAQUE
+SUMMARY:Simple event
+DTSTART;TZID=America/New_York:20101021T120000
+DTSTAMP:20101018T155438Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+"""
+
+INVITED_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
+DTSTART;TZID=America/New_York:20110131T130000
+DTEND;TZID=America/New_York:20110131T140000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=NE
+ EDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:urn:uuid:user02
+CREATED:20110124T170357Z
+DTSTAMP:20110124T170425Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:3
+SUMMARY:Some Event For You
+TRANSP:TRANSPARENT
+X-APPLE-NEEDS-REPLY:TRUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+ACCEPTED_EVENT = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:882C3D50-0DAE-45CB-A2E7-DA75DA9BE452
+DTSTART;TZID=America/New_York:20110131T130000
+DTEND;TZID=America/New_York:20110131T140000
+ATTENDEE;CN=User 01;CUTYPE=INDIVIDUAL;EMAIL=user01 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user01
+ATTENDEE;CN=User 02;CUTYPE=INDIVIDUAL;EMAIL=user02 at example.com;PARTSTAT=AC
+ CEPTED:urn:uuid:user02
+CREATED:20110124T170357Z
+DTSTAMP:20110124T170425Z
+ORGANIZER;CN=User 01;EMAIL=user01 at example.com:urn:uuid:user01
+SEQUENCE:3
+SUMMARY:Some Event For You
+TRANSP:TRANSPARENT
+X-APPLE-NEEDS-REPLY:TRUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+
+
+class Deterministic(object):
+    def gauss(self, mean, stddev):
+        """
+        Pretend to return a value from a gaussian distribution with mu
+        parameter C{mean} and sigma parameter C{stddev}.  But actually
+        always return C{mean + 1}.
+        """
+        return mean + 1
+
+
+    def choice(self, sequence):
+        return sequence[0]
+
+
+
+class StubClient(object):
+    def __init__(self, number):
+        self._events = {}
+        self._calendars = {}
+        self.user = u"user%02d" % (number,)
+        self.email = u"mailto:user%02d at example.com" % (number,)
+
+
+    def addEventAttendee(self, href, attendee):
+        vevent = self._events[href].vevent
+        attendees = vevent.contents[u'vevent'][0].contents.setdefault(u'attendee', [])
+        attendees.append(attendee)
+
+
+    def changeEventAttendee(self, href, old, new):
+        vevent = self._events[href].vevent
+        attendees = vevent.contents[u'vevent'][0].contents.setdefault(u'attendee', [])
+        attendees.remove(old)
+        attendees.append(new)
+
+
+
+class InviterTests(TestCase):
+    """
+    Tests for loadtest.profiles.Inviter.
+    """
+    def _simpleAccount(self, userNumber, eventText):
+        vevent = list(readComponents(eventText))[0]
+        calendar = Calendar(
+            caldavxml.calendar, u'calendar', u'/cal/', None)
+        event = Event(calendar.url + u'1234.ics', None, vevent)
+        calendar.events = {u'1234.ics': event}
+        client = StubClient(userNumber)
+        client._events.update({event.url: event})
+        client._calendars.update({calendar.url: calendar})
+
+        return vevent, event, calendar, client
+
+
+
+    def test_doNotAddAttendeeToInbox(self):
+        """
+        When the only calendar with any events is a schedule inbox, no
+        attempt is made to add attendees to an event on that calendar.
+        """
+        userNumber = 10
+        vevent, event, calendar, client = self._simpleAccount(
+            userNumber, SIMPLE_EVENT)
+        calendar.resourceType = caldavxml.schedule_inbox
+        inviter = Inviter(None, client, userNumber)
+        inviter._invite()
+        self.assertNotIn(u'attendee', vevent.contents[u'vevent'][0].contents)
+
+
+    def test_doNotAddAttendeeToNoCalendars(self):
+        """
+        When there are no calendars and no events at all, the inviter
+        does nothing.
+        """
+        userNumber = 13
+        client = StubClient(userNumber)
+        inviter = Inviter(None, client, userNumber)
+        inviter._invite()
+        self.assertEquals(client._events, {})
+        self.assertEquals(client._calendars, {})
+
+
+    def test_addAttendeeToEvent(self):
+        """
+        When there is a normal calendar with an event, inviter adds an
+        attendee to it.
+        """
+        userNumber = 16
+        vevent, event, calendar, client = self._simpleAccount(
+            userNumber, SIMPLE_EVENT)
+        inviter = Inviter(None, client, userNumber)
+        inviter.random = Deterministic()
+        inviter._invite()
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        self.assertEquals(len(attendees), 1)
+        self.assertEquals(attendees[0].params, {
+                u'CN': [u'User %d' % (userNumber + 1,)],
+                u'CUTYPE': [u'INDIVIDUAL'],
+                u'EMAIL': [u'user%d at example.com' % (userNumber + 1,)],
+                u'PARTSTAT': [u'NEEDS-ACTION'],
+                u'ROLE': [u'REQ-PARTICIPANT'],
+                u'RSVP': [u'TRUE']})
+
+
+
+    def test_doNotAddSelfToEvent(self):
+        """
+        If the inviter randomly selects its own user to be added to
+        the attendee list, a different user is added instead.
+        """
+        selfNumber = 12
+        vevent, event, calendar, client = self._simpleAccount(
+            selfNumber, SIMPLE_EVENT)
+
+        otherNumber = 20
+        values = [selfNumber, otherNumber]
+
+        inviter = Inviter(None, client, selfNumber)
+        inviter.random = Deterministic()
+        inviter.random.gauss = lambda mu, sigma: values.pop(0)
+        inviter._invite()
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        self.assertEquals(len(attendees), 1)
+        self.assertEquals(attendees[0].params, {
+                u'CN': [u'User %d' % (otherNumber,)],
+                u'CUTYPE': [u'INDIVIDUAL'],
+                u'EMAIL': [u'user%d at example.com' % (otherNumber,)],
+                u'PARTSTAT': [u'NEEDS-ACTION'],
+                u'ROLE': [u'REQ-PARTICIPANT'],
+                u'RSVP': [u'TRUE']})
+
+
+
+    def test_doNotAddExistingToEvent(self):
+        """
+        If the inviter randomly selects a user which is already an
+        invitee on the event, a different user is added instead.
+        """
+        selfNumber = 13
+        vevent, event, calendar, client = self._simpleAccount(
+            selfNumber, INVITED_EVENT)
+
+        invitee = vevent.contents[u'vevent'][0].contents[u'attendee'][0]
+        inviteeNumber = int(invitee.params[u'CN'][0].split()[1])
+        anotherNumber = inviteeNumber + 5
+        values = [inviteeNumber, anotherNumber]
+
+        inviter = Inviter(None, client, selfNumber)
+        inviter.random = Deterministic()
+        inviter.random.gauss = lambda mu, sigma: values.pop(0)
+        inviter._invite()
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        self.assertEquals(len(attendees), 3)
+        self.assertEquals(attendees[2].params, {
+                u'CN': [u'User %02d' % (anotherNumber,)],
+                u'CUTYPE': [u'INDIVIDUAL'],
+                u'EMAIL': [u'user%02d at example.com' % (anotherNumber,)],
+                u'PARTSTAT': [u'NEEDS-ACTION'],
+                u'ROLE': [u'REQ-PARTICIPANT'],
+                u'RSVP': [u'TRUE']})
+
+
+
+class AccepterTests(TestCase):
+    """
+    Tests for loadtest.profiles.Accepter.
+    """
+    def test_ignoreEventOnUnknownCalendar(self):
+        """
+        If an event on an unknown calendar changes, it is ignored.
+        """
+        userNumber = 13
+        client = StubClient(userNumber)
+        accepter = Accepter(None, client, userNumber)
+        accepter.eventChanged('/some/calendar/1234.ics')
+
+
+
+    def test_ignoreNonCalendar(self):
+        """
+        If an event is on a calendar which is not of type
+        {CALDAV:}calendar, it is ignored.
+        """
+        userNumber = 14
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.schedule_inbox, u'inbox', calendarURL, None)
+        client = StubClient(userNumber)
+        client._calendars[calendarURL] = calendar
+        accepter = Accepter(None, client, userNumber)
+        accepter.eventChanged(calendarURL + '1234.ics')
+
+
+    def test_ignoreAccepted(self):
+        """
+        If the client is an attendee on an event but the PARTSTAT is
+        not NEEDS-ACTION, the event is ignored.
+        """
+        vevent = list(readComponents(ACCEPTED_EVENT))[0]
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        userNumber = int(attendees[1].params[u'CN'][0].split(None, 1)[1])
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, u'calendar', calendarURL, None)
+        client = StubClient(userNumber)
+        client._calendars[calendarURL] = calendar
+        event = Event(calendarURL + u'1234.ics', None, vevent)
+        client._events[event.url] = event
+        accepter = Accepter(None, client, userNumber)
+        accepter.eventChanged(event.url)
+
+
+    def test_acceptInvitation(self):
+        """
+        If the client is an attendee on an event and the PARTSTAT is
+        NEEDS-ACTION, a response is generated which accepts the
+        invitation.
+        """
+        clock = Clock()
+        randomDelay = 7
+        vevent = list(readComponents(INVITED_EVENT))[0]
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        userNumber = int(attendees[1].params[u'CN'][0].split(None, 1)[1])
+        calendarURL = '/some/calendar/'
+        calendar = Calendar(
+            caldavxml.calendar, u'calendar', calendarURL, None)
+        client = StubClient(userNumber)
+        client._calendars[calendarURL] = calendar
+        event = Event(calendarURL + u'1234.ics', None, vevent)
+        client._events[event.url] = event
+        accepter = Accepter(clock, client, userNumber)
+        accepter.random = Deterministic()
+        accepter.random.gauss = lambda mu, sigma: randomDelay
+        accepter.eventChanged(event.url)
+        clock.advance(randomDelay)
+
+        vevent = client._events[event.url].vevent
+        attendees = vevent.contents[u'vevent'][0].contents[u'attendee']
+        self.assertEquals(len(attendees), 2)
+        self.assertEquals(
+            attendees[1].params[u'CN'], [u'User %02d' % (userNumber,)])
+        self.assertEquals(
+            attendees[1].params[u'PARTSTAT'], [u'ACCEPTED'])
+        self.assertNotIn(u'RSVP', attendees[1].params)
+
+
+        
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20110128/e8d16889/attachment-0001.html>


More information about the calendarserver-changes mailing list