[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