[CalendarServer-changes] [9231] CalendarServer/branches/users/gaya/sharedabgroups

source_changes at macosforge.org source_changes at macosforge.org
Mon May 7 11:12:04 PDT 2012


Revision: 9231
          http://trac.macosforge.org/projects/calendarserver/changeset/9231
Author:   gaya at apple.com
Date:     2012-05-07 11:12:03 -0700 (Mon, 07 May 2012)
Log Message:
-----------
update to trunk

Modified Paths:
--------------
    CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/calverify.py
    CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/test/test_calverify.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql.py
    CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/test/util.py

Added Paths:
-----------
    CalendarServer/branches/users/gaya/sharedabgroups/contrib/tools/harpoon.py

Modified: CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/calverify.py	2012-05-04 21:53:03 UTC (rev 9230)
+++ CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/calverify.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -52,11 +52,14 @@
 from twisted.python import log
 from twisted.python.text import wordWrap
 from twisted.python.usage import Options
+from twistedcaldav import caldavxml
 from twistedcaldav.dateops import pyCalendarTodatetime
 from twistedcaldav.ical import Component, ignoredComponents,\
-    InvalidICalendarDataError
+    InvalidICalendarDataError, Property
+from twistedcaldav.scheduling.itip import iTipGenerator
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 from twistedcaldav.util import normalizationLookup
+from txdav.base.propertystore.base import PropertyName
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
 from txdav.common.icommondatastore import InternalDataStoreError
 import base64
@@ -65,8 +68,9 @@
 import sys
 import time
 import traceback
+import uuid
 
-VERSION = "2"
+VERSION = "3"
 
 def usage(e=None):
     if e:
@@ -153,6 +157,22 @@
     Service which runs, exports the appropriate records, then stops the reactor.
     """
 
+    metadata = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
+    metadata_inbox = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": False,
+        "scheduleTag": "",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
     def __init__(self, store, options, output, reactor, config):
         super(CalVerifyService, self).__init__()
         self.store   = store
@@ -167,6 +187,11 @@
         
         self.results = {}
         self.summary = []
+        self.fixAttendeesForOrganizerMissing = 0
+        self.fixAttendeesForOrganizerMismatch = 0
+        self.fixOrganizersForAttendeeMissing = 0
+        self.fixOrganizersForAttendeeMismatch = 0
+        self.fixFailed = 0
         self.total = 0
         self.totalErrors = None
         self.totalExceptions = None
@@ -327,11 +352,11 @@
 
 
     @inlineCallbacks
-    def doScan(self, ical, mismatch, fix):
+    def doScan(self, ical, mismatch, fix, start=None):
         
         self.output.write("\n---- Scanning calendar data ----\n")
 
-        self.start = PyCalendarDateTime.getToday()
+        self.start = start if start is not None else PyCalendarDateTime.getToday()
         self.start.setDateOnly(False)
         self.end = self.start.duplicate()
         self.end.offsetYear(1)
@@ -377,33 +402,8 @@
         self.attended = []
         self.attended_byuid = collections.defaultdict(list)
         self.matched_attendee_to_organizer = collections.defaultdict(set)
-        skipped = 0
-        inboxes = 0
-        for owner, resid, uid, calname, md5, organizer, created, modified in rows:
-            
-            # Skip owners not enabled for calendaring
-            if not self.testForCalendaringUUID(owner):
-                skipped += 1
-                continue
-
-            # Skip inboxes
-            if calname == "inbox":
-                inboxes += 1
-                continue
-
-            # If targeting a specific organizer, skip events belonging to others
-            if self.options["uuid"]:
-                if not organizer.startswith("urn:uuid:") or self.options["uuid"] != organizer[9:]:
-                    continue
+        skipped, inboxes = self.buildResourceInfo(rows)
                 
-            # Cache organizer/attendee states
-            if organizer.startswith("urn:uuid:") and owner == organizer[9:]:
-                self.organized.append((owner, resid, uid, md5, organizer, created, modified,))
-                self.organized_byuid[uid] = (owner, resid, uid, md5, organizer, created, modified,)
-            else:
-                self.attended.append((owner, resid, uid, md5, organizer, created, modified,))
-                self.attended_byuid[uid].append((owner, resid, uid, md5, organizer, created, modified,))
-                
         self.output.write("Number of organizer events to process: %s\n" % (len(self.organized),))
         self.output.write("Number of attendee events to process: %s\n" % (len(self.attended,)))
         self.results["Number of organizer events to process"] = len(self.organized)
@@ -423,6 +423,20 @@
             self.totalErrors = 0
             yield self.verifyAllAttendeesForOrganizer()
             yield self.verifyAllOrganizersForAttendee()
+            
+            # Need to add fix summary information
+            if fix:
+                self.addSummaryBreak()
+                self.results["Fixed missing attendee events"] = self.fixAttendeesForOrganizerMissing
+                self.results["Fixed mismatched attendee events"] = self.fixAttendeesForOrganizerMismatch
+                self.results["Fixed missing organizer events"] = self.fixOrganizersForAttendeeMissing
+                self.results["Fixed mismatched organizer events"] = self.fixOrganizersForAttendeeMismatch
+                self.results["Fix failures"] = self.fixFailed
+                self.addToSummary("Fixed missing attendee events", self.fixAttendeesForOrganizerMissing)
+                self.addToSummary("Fixed mismatched attendee events", self.fixAttendeesForOrganizerMismatch)
+                self.addToSummary("Fixed missing organizer events", self.fixOrganizersForAttendeeMissing)
+                self.addToSummary("Fixed mismatched organizer events", self.fixOrganizersForAttendeeMismatch)
+                self.addToSummary("Fix failures", self.fixFailed)
         
         yield succeed(None)
 
@@ -552,6 +566,38 @@
         returnValue(rows[0])
 
 
+    def buildResourceInfo(self, rows, onlyOrganizer=False, onlyAttendee=False):
+        skipped = 0
+        inboxes = 0
+        for owner, resid, uid, calname, md5, organizer, created, modified in rows:
+            
+            # Skip owners not enabled for calendaring
+            if not self.testForCalendaringUUID(owner):
+                skipped += 1
+                continue
+
+            # Skip inboxes
+            if calname == "inbox":
+                inboxes += 1
+                continue
+
+            # If targeting a specific organizer, skip events belonging to others
+            if self.options["uuid"]:
+                if not organizer.startswith("urn:uuid:") or self.options["uuid"] != organizer[9:]:
+                    continue
+                
+            # Cache organizer/attendee states
+            if organizer.startswith("urn:uuid:") and owner == organizer[9:]:
+                if not onlyAttendee:
+                    self.organized.append((owner, resid, uid, md5, organizer, created, modified,))
+                    self.organized_byuid[uid] = (owner, resid, uid, md5, organizer, created, modified,)
+            else:
+                if not onlyOrganizer:
+                    self.attended.append((owner, resid, uid, md5, organizer, created, modified,))
+                    self.attended_byuid[uid].append((owner, resid, uid, md5, organizer, created, modified,))
+        
+        return skipped, inboxes
+
     def testForCalendaringUUID(self, uuid):
         """
         Determine if the specified directory UUID is valid for calendaring. Keep a cache of
@@ -900,12 +946,12 @@
             # Get attendee states for matching UID
             eachAttendeesOwnStatus = {}
             for attendeeEvent in self.attended_byuid.get(uid, ()):
-                owner, attresid, uid, _ignore_md5, _ignore_organizer, att_created, att_modified = attendeeEvent
+                owner, attresid, attuid, _ignore_md5, _ignore_organizer, att_created, att_modified = attendeeEvent
                 calendar = yield self.getCalendar(attresid)
                 if calendar is None:
                     continue
                 eachAttendeesOwnStatus[owner] = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=owner)
-                attendeeResIDs[(owner, uid)] = attresid
+                attendeeResIDs[(owner, attuid)] = attresid
             
             # Look at each attendee in the organizer's meeting
             for organizerAttendee, organizerViewOfStatus in organizerViewOfAttendees.iteritems():
@@ -917,6 +963,15 @@
                 if not self.testForCalendaringUUID(organizerAttendee):
                     continue
 
+                # Double check the missing attendee situation in case we missed it during the original query
+                if organizerAttendee not in eachAttendeesOwnStatus:
+                    # Try to reload the attendee data
+                    calendar, attresid = yield self.getCalendarForOwnerByUID(organizerAttendee, uid)
+                    if calendar is not None:
+                        eachAttendeesOwnStatus[organizerAttendee] = self.buildAttendeeStates(calendar, self.start, self.end, attendee_only=organizerAttendee)
+                        attendeeResIDs[(organizerAttendee, uid)] = attresid
+                        #print "Reloaded missing attendee data"
+                     
                 # If an entry for the attendee exists, then check whether attendee status matches
                 if organizerAttendee in eachAttendeesOwnStatus:
                     attendeeOwnStatus = eachAttendeesOwnStatus[organizerAttendee].get(organizerAttendee, set())
@@ -926,6 +981,7 @@
                         for _organizerInstance, partstat in organizerViewOfStatus.difference(attendeeOwnStatus):
                             if partstat not in ("DECLINED", "CANCELLED"):
                                 results_mismatch.append((uid, resid, organizer, org_created, org_modified, organizerAttendee, att_created, att_modified))
+                                self.results.setdefault("Mismatch Attendee", set()).add((uid, organizer, organizerAttendee,))
                                 broken = True
                                 if self.options["details"]:
                                     self.output.write("Mismatch: on Organizer's side:\n")
@@ -939,6 +995,7 @@
                             if partstat not in ("CANCELLED",):
                                 if not broken:
                                     results_mismatch.append((uid, resid, organizer, org_created, org_modified, organizerAttendee, att_created, att_modified))
+                                    self.results.setdefault("Mismatch Attendee", set()).add((uid, organizer, organizerAttendee,))
                                 broken = True
                                 if self.options["details"]:
                                     self.output.write("Mismatch: on Attendee's side:\n")
@@ -952,14 +1009,13 @@
                     for _ignore_instance_id, partstat in organizerViewOfStatus:
                         if partstat not in ("DECLINED", "CANCELLED"):
                             results_missing.append((uid, resid, organizer, organizerAttendee, org_created, org_modified))
+                            self.results.setdefault("Missing Attendee", set()).add((uid, organizer, organizerAttendee,))
                             broken = True
                             break
                 
                 # If there was a problem we can fix it
                 if broken and self.fix:
-                    # TODO: This is where we attempt a fix
-                    #self.fixEvent(organizer, organizerAttendee, eventpath, attendeePaths.get(organizerAttendee, None))
-                    pass
+                    yield self.fixByReinvitingAttendee(resid, attendeeResIDs.get((organizerAttendee, uid)), organizerAttendee)
 
         yield self.txn.commit()
         self.txn = None
@@ -1032,7 +1088,7 @@
         attended_div = 1 if attended_len < 100 else attended_len / 100
 
         t = time.time()
-        for ctr, attendeeEvent in enumerate(self.attended):
+        for ctr, attendeeEvent in enumerate(tuple(self.attended)): # self.attended might mutate during the loop
             
             if self.options["verbose"] and divmod(ctr, attended_div)[1] == 0:
                 self.output.write(("\r%d of %d (%d%%) Missing: %d  Mismatched: %s" % (
@@ -1067,18 +1123,32 @@
             if not self.testForCalendaringUUID(organizer):
                 continue
 
+            # Double check the missing attendee situation in case we missed it during the original query
             if uid not in self.organized_byuid:
+                # Try to reload the organizer info data
+                rows = yield self.getAllResourceInfoWithUID(uid)
+                self.buildResourceInfo(rows, onlyOrganizer=True)
+                
+                #if uid in self.organized_byuid:
+                #    print "Reloaded missing organizer data: %s" % (uid,)
+                 
+            if uid not in self.organized_byuid:
 
                 # Check whether attendee has all instances cancelled
                 if self.allCancelled(eachAttendeesOwnStatus):
                     continue
                 
                 missing.append((uid, attendee, organizer, resid, att_created, att_modified,))
+                self.results.setdefault("Missing Organizer", set()).add((uid, attendee, organizer,))
                 
                 # If there is a miss we fix by removing the attendee data
                 if self.fix:
-                    # TODO: This is where we attempt a fix
-                    pass
+                    # This is where we attempt a fix
+                    fix_result = (yield self.fixByRemovingEvent(resid))
+                    if fix_result:
+                        self.fixOrganizersForAttendeeMissing += 1
+                    else:
+                        self.fixFailed += 1 
 
             elif attendee not in self.matched_attendee_to_organizer[uid]:
                 # Check whether attendee has all instances cancelled
@@ -1086,11 +1156,11 @@
                     continue
 
                 mismatched.append((uid, attendee, organizer, resid, att_created, att_modified,))
+                self.results.setdefault("Mismatch Organizer", set()).add((uid, attendee, organizer,))
                 
                 # If there is a mismatch we fix by re-inviting the attendee
                 if self.fix:
-                    # TODO: This is where we attempt a fix
-                    pass
+                    yield self.fixByReinvitingAttendee(self.organized_byuid[uid][1], resid, attendee)
 
         yield self.txn.commit()
         self.txn = None
@@ -1153,6 +1223,124 @@
         self.totalErrors += len(mismatched)
 
 
+    @inlineCallbacks
+    def fixByReinvitingAttendee(self, orgresid, attresid, attendee):
+        """
+        Fix a mismatch/missing error by having the organizer send a REQUEST for the entire event to the attendee
+        to trigger implicit scheduling to resync the attendee event.
+        
+        We do not have implicit apis in the store, but really want to use store-only apis here to avoid having to create
+        "fake" HTTP requests and manipulate HTTP resources. So what we will do is emulate implicit behavior by copying the
+        organizer resource to the attendee (filtering it for the attendee's view of the event) and deposit an inbox item
+        for the same event. Right now that will wipe out any per-attendee data - notably alarms.
+        """
+        
+        try:
+            cuaddr = "urn:uuid:%s" % attendee
+    
+            # Get the organizer's calendar data
+            calendar = (yield self.getCalendar(orgresid))
+            calendar = Component(None, pycalendar=calendar)
+            
+            # Generate an iTip message for the entire event filtered for the attendee's view
+            itipmsg = iTipGenerator.generateAttendeeRequest(calendar, (cuaddr,), None)
+            
+            # Handle the case where the attendee is not actually in the organizer event at all by
+            # removing the attendee event instead of re-inviting
+            if itipmsg.resourceUID() is None:
+                yield self.fixByRemovingEvent(attresid)
+                returnValue(True)
+
+            # Convert iTip message into actual calendar data - just remove METHOD
+            attendee_calendar = itipmsg.duplicate()
+            attendee_calendar.removeProperty(attendee_calendar.getProperty("METHOD"))
+            
+            # Adjust TRANSP to match PARTSTAT
+            self.setTransparencyForAttendee(attendee_calendar, cuaddr)
+    
+            # Get attendee home store object
+            home = (yield self.txn.calendarHomeWithUID(attendee))
+            if home is None:
+                raise ValueError("Cannot find home")
+            inbox = (yield home.calendarWithName("inbox"))
+            if inbox is None:
+                raise ValueError("Cannot find inbox")
+    
+            # Replace existing resource data, or create a new one
+            if attresid:
+                # TODO: transfer over per-attendee data - valarms
+                _ignore_homeID, calendarID = yield self.getAllResourceInfoForResourceID(attresid)
+                calendar = yield home.childWithID(calendarID)
+                calendarObj = yield calendar.objectResourceWithID(attresid)
+                calendarObj.scheduleTag = str(uuid.uuid4())
+                yield calendarObj.setComponent(attendee_calendar)
+                self.results.setdefault("Fix change event", set()).add((home.name(), calendar.name(), attendee_calendar.resourceUID(),))
+            else:
+                # Find default calendar for VEVENTs
+                defaultCalendar = (yield self.defaultCalendarForAttendee(home, inbox))
+                if defaultCalendar is None:
+                    raise ValueError("Cannot find suitable default calendar")
+                yield defaultCalendar.createCalendarObjectWithName(str(uuid.uuid4()) + ".ics", attendee_calendar, self.metadata)
+                self.results.setdefault("Fix add event", set()).add((home.name(), defaultCalendar.name(), attendee_calendar.resourceUID(),))
+            
+            # Write new itip message to attendee inbox
+            yield inbox.createCalendarObjectWithName(str(uuid.uuid4()) + ".ics", itipmsg, self.metadata_inbox)
+            self.results.setdefault("Fix add inbox", set()).add((home.name(), itipmsg.resourceUID(),))
+     
+            yield self.txn.commit()
+            self.txn = self.store.newTransaction()
+    
+            returnValue(True)
+
+        except Exception, e:
+            print "Failed to fix resource: %d for attendee: %s\n%s" % (orgresid, attendee, e,)
+            returnValue(False)
+        
+
+    @inlineCallbacks
+    def defaultCalendarForAttendee(self, home, inbox):
+        
+        # Check for property
+        default = inbox.properties().get(PropertyName.fromElement(caldavxml.ScheduleDefaultCalendarURL))
+        if default:
+            defaultName = str(default.children[0]).rstrip("/").split("/")[-1]
+            defaultCalendar = (yield home.calendarWithName(defaultName))
+            returnValue(defaultCalendar)
+        else:
+            # Iterate for the first calendar that supports VEVENTs
+            calendars = (yield home.calendars())
+            for calendar in calendars:
+                if calendar.name() != "inbox" and calendar.isSupportedComponent("VEVENT"):
+                    returnValue(calendar)
+            else:
+                returnValue(None)
+
+
+    @inlineCallbacks
+    def fixByRemovingEvent(self, resid):
+        """
+        Remove the calendar resource specified by resid - this is a force remove - no implicit
+        scheduling is required so we use store apis directly.
+        """
+
+        try:
+            homeID, calendarID = yield self.getAllResourceInfoForResourceID(resid)
+            home = yield self.txn.calendarHomeWithResourceID(homeID)
+            calendar = yield home.childWithID(calendarID)
+            calendarObj = yield calendar.objectResourceWithID(resid)
+            objname = calendarObj.name()
+            yield calendar._removeObjectResource(calendarObj)
+            yield self.txn.commit()
+            self.txn = self.store.newTransaction()
+            
+            self.results.setdefault("Fix remove", set()).add((home.name(), calendar.name(), objname,))
+
+            returnValue(True)
+        except Exception, e:
+            print "Failed to remove resource whilst fixing: %d\n%s" % (resid, e,)
+            returnValue(False)
+        
+
     def addToSummary(self, title, count, total=None):
         if total is not None:
             percent = safePercent(count, total),
@@ -1223,6 +1411,31 @@
         returnValue(caldata)
 
 
+    @inlineCallbacks
+    def getCalendarForOwnerByUID(self, owner, uid):
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        ch = schema.CALENDAR_HOME
+        
+        kwds = { "OWNER" : owner, "UID": uid }
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT, co.RESOURCE_ID,],
+            From=ch.join(
+                cb, type="inner", on=(ch.RESOURCE_ID == cb.CALENDAR_HOME_RESOURCE_ID)).join(
+                co, type="inner", on=(cb.CALENDAR_RESOURCE_ID == co.CALENDAR_RESOURCE_ID).And(
+                    cb.BIND_MODE == _BIND_MODE_OWN).And(
+                    cb.CALENDAR_RESOURCE_NAME != "inbox")),
+            Where=(ch.OWNER_UID == Parameter("OWNER")).And(co.ICALENDAR_UID == Parameter("UID")),
+        ).on(self.txn, **kwds))
+
+        try:
+            caldata = PyCalendar.parseText(rows[0][0]) if rows else None
+        except PyCalendarError:
+            returnValue((None, None,))
+
+        returnValue((caldata, rows[0][1] if rows else None,))
+
+
     def masterComponent(self, calendar):
         """
         Return the master iCal component in this calendar.
@@ -1302,6 +1515,21 @@
         return all_cancelled
        
 
+    def setTransparencyForAttendee(self, calendar, attendee):
+        """
+        Set the TRANSP property based on the PARTSTAT value on matching ATTENDEE properties
+        in each component.
+        """
+        for component in calendar.subcomponents():
+            if component.name() in ignoredComponents:
+                continue
+            prop = component.getAttendeeProperty(attendee)
+            addTransp = False
+            if prop:
+                partstat = prop.parameterValue("PARTSTAT", "NEEDS-ACTION")
+                addTransp = partstat in ("NEEDS-ACTION", "DECLINED",)
+            component.replaceProperty(Property("TRANSP", "TRANSPARENT" if addTransp else "OPAQUE"))
+
     def directoryService(self):
         """
         Get an appropriate directory service for this L{CalVerifyService}'s

Modified: CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/test/test_calverify.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/test/test_calverify.py	2012-05-04 21:53:03 UTC (rev 9230)
+++ CalendarServer/branches/users/gaya/sharedabgroups/calendarserver/tools/test/test_calverify.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -21,12 +21,16 @@
 from StringIO import StringIO
 from calendarserver.tap.util import getRootResource
 from calendarserver.tools.calverify import CalVerifyService
+from pycalendar.datetime import PyCalendarDateTime
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.trial import unittest
+from twistedcaldav import caldavxml
 from twistedcaldav.config import config
+from txdav.base.propertystore.base import PropertyName
 from txdav.caldav.datastore import util
 from txdav.common.datastore.test.util import buildStore, populateCalendarsFrom, CommonCommonTests
+from txdav.xml import element as davxml
 import os
 
 
@@ -333,9 +337,9 @@
 """.replace("\n", "\r\n")
 
 
-class CalVerifyTests(CommonCommonTests, unittest.TestCase):
+class CalVerifyDataTests(CommonCommonTests, unittest.TestCase):
     """
-    Tests for deleting events older than a given date
+    Tests calverify for iCalendar data problems.
     """
 
     metadata = {
@@ -365,7 +369,7 @@
 
     @inlineCallbacks
     def setUp(self):
-        yield super(CalVerifyTests, self).setUp()
+        yield super(CalVerifyDataTests, self).setUp()
         self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
         yield self.populate()
 
@@ -795,3 +799,687 @@
             okbase64 = okbase64.replace("\r\n ", "")
             self.assertEqual(calverifyNo64.fixBadOldCuaLines(bad), oknobase64)
             self.assertEqual(calverify64.fixBadOldCuaLines(bad), okbase64)
+
+
+class CalVerifyMismatchTestsBase(CommonCommonTests, unittest.TestCase):
+    """
+    Tests calverify for iCalendar mismatch problems.
+    """
+
+    metadata = {
+        "accessMode": "PUBLIC",
+        "isScheduleObject": True,
+        "scheduleTag": "abc",
+        "scheduleEtags": (),
+        "hasPrivateComment": False,
+    }
+
+    uuid1 = "D46F3D71-04B7-43C2-A7B6-6F92F92E61D0"
+    uuid2 = "47B16BB4-DB5F-4BF6-85FE-A7DA54230F92"
+    uuid3 = "AC478592-7783-44D1-B2AE-52359B4E8415"
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(CalVerifyMismatchTestsBase, self).setUp()
+        self._sqlCalendarStore = yield buildStore(self, self.notifierFactory)
+        yield self.populate()
+
+        inbox = (yield self.calendarUnderTest(self.uuid3, "inbox"))
+        inbox.properties()[PropertyName.fromElement(caldavxml.ScheduleDefaultCalendarURL)] = caldavxml.ScheduleDefaultCalendarURL(
+            davxml.HRef.fromString("/calendars/__uids__/%s/calendar2/" % (self.uuid3,))
+        )
+        yield self.commit()
+
+        self.patch(config.DirectoryService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "calverify", "accounts.xml"
+            )
+        )
+        self.patch(config.ResourceService.params, "xmlFile",
+            os.path.join(
+                os.path.dirname(__file__), "calverify", "resources.xml"
+            )
+        )
+        self.rootResource = getRootResource(config, self._sqlCalendarStore)
+        self.directory = self.rootResource.getDirectory()
+
+
+    @inlineCallbacks
+    def populate(self):
+        
+        # Need to bypass normal validation inside the store
+        util.validationBypass = True
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest(), migrating=True)
+        util.validationBypass = False
+        self.notifierFactory.reset()
+
+
+    def storeUnderTest(self):
+        """
+        Create and return a L{CalendarStore} for testing.
+        """
+        return self._sqlCalendarStore
+
+
+    @inlineCallbacks
+    def homeUnderTest(self, name=None, txn=None):
+        """
+        Get the calendar home detailed by C{requirements[name]}.
+        """
+        if txn is None:
+            txn = self.transactionUnderTest()
+        returnValue((yield txn.calendarHomeWithUID(name)))
+
+
+    @inlineCallbacks
+    def calendarUnderTest(self, home_name, name="calendar", txn=None):
+        """
+        Get the calendar detailed by C{requirements[home_name][name]}.
+        """
+        returnValue((yield
+            (yield self.homeUnderTest(home_name, txn)).calendarWithName(name))
+        )
+
+
+    @inlineCallbacks
+    def calendarObjectUnderTest(self, home_name, calendar_name, name, txn=None):
+        """
+        Get the calendar object detailed by C{requirements[home_name][calendar_name][name]}.
+        """
+        returnValue((yield
+            (yield self.calendarUnderTest(home_name, calendar_name, txn)).calendarObjectWithName(name))
+        )
+
+
+class CalVerifyMismatchTestsNonRecurring(CalVerifyMismatchTestsBase):
+    """
+    Tests calverify for iCalendar mismatch problems for non-recurring events.
+    """
+
+    # Organizer has event, attendees do not
+    MISSING_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISSING_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Attendees have event, organizer does not
+    MISSING_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISSING_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISSING_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISSING_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Attendee partstat mismatch
+    MISMATCH_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Attendee events outside time range
+    MISMATCH2_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH2_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:19990307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH2_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:19990307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Organizer event outside time range
+    MISMATCH_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:19990307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Attendee uuid3 has event with different organizer
+    MISMATCH3_ATTENDEE_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH3_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH3_ATTENDEE_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH3_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH3_ATTENDEE_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH3_ATTENDEE_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=NEEDS-ACTION:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    # Attendee uuid3 has event they are not invited to
+    MISMATCH2_ORGANIZER_1_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH2_ORGANIZER_2_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+    MISMATCH2_ORGANIZER_3_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 4.0.1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20100303T181216Z
+UID:MISMATCH2_ORGANIZER_ICS
+DTEND:20000307T151500Z
+TRANSP:OPAQUE
+SUMMARY:Ancient event
+DTSTART:20000307T111500Z
+DTSTAMP:20100303T181220Z
+SEQUENCE:2
+ORGANIZER:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:D46F3D71-04B7-43C2-A7B6-6F92F92E61D0
+ATTENDEE;PARTSTAT=DECLINED:urn:uuid:47B16BB4-DB5F-4BF6-85FE-A7DA54230F92
+ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:AC478592-7783-44D1-B2AE-52359B4E8415
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+
+    requirements = {
+        CalVerifyMismatchTestsBase.uuid1 : {
+            "calendar" : {
+                 "missing_attendee.ics"      : (MISSING_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+                 "mismatched_attendee.ics"   : (MISMATCH_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+                 "mismatched2_attendee.ics"  : (MISMATCH2_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+                 "mismatched3_attendee.ics"  : (MISMATCH3_ATTENDEE_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+                 "mismatched_organizer.ics"  : (MISMATCH_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+                 "mismatched2_organizer.ics" : (MISMATCH2_ORGANIZER_1_ICS, CalVerifyMismatchTestsBase.metadata,),
+           },
+           "inbox" : {},
+        },
+        CalVerifyMismatchTestsBase.uuid2 : {
+            "calendar" : {
+                "mismatched_attendee.ics"   : (MISMATCH_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched2_attendee.ics"  : (MISMATCH2_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched3_attendee.ics"  : (MISMATCH3_ATTENDEE_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "missing_organizer.ics"     : (MISSING_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched_organizer.ics"  : (MISMATCH_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched2_organizer.ics" : (MISMATCH2_ORGANIZER_2_ICS, CalVerifyMismatchTestsBase.metadata,),
+            },
+           "inbox" : {},
+        },
+        CalVerifyMismatchTestsBase.uuid3 : {
+            "calendar" : {
+                "mismatched_attendee.ics"   : (MISMATCH_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched3_attendee.ics"  : (MISMATCH3_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "missing_organizer.ics"     : (MISSING_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched2_organizer.ics" : (MISMATCH2_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+            },
+            "calendar2" : {
+                "mismatched_organizer.ics" : (MISMATCH_ORGANIZER_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+                "mismatched2_attendee.ics" : (MISMATCH2_ATTENDEE_3_ICS, CalVerifyMismatchTestsBase.metadata,),
+            },
+           "inbox" : {},
+        },
+    }
+
+    @inlineCallbacks
+    def test_scanMismatchOnly(self):
+        """
+        CalVerifyService.doScan without fix for mismatches. Make sure it detects
+        as much as it can. Make sure sync-token is not changed.
+        """
+
+        sync_token_old1 = (yield (yield self.calendarUnderTest(self.uuid1)).syncToken())
+        sync_token_old2 = (yield (yield self.calendarUnderTest(self.uuid2)).syncToken())
+        sync_token_old3 = (yield (yield self.calendarUnderTest(self.uuid3)).syncToken())
+        self.commit()
+
+        options = {
+            "ical":False,
+            "badcua":False,
+            "mismatch":True,
+            "nobase64":False,
+            "verbose":False,
+            "details":False,
+            "uid":"",
+            "uuid":"",
+        }
+        output = StringIO()
+        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doScan(False, True, False, start=PyCalendarDateTime(2000, 1, 1, 0, 0, 0))
+
+        self.assertEqual(calverify.results["Number of events to process"], 15)
+        self.assertEqual(calverify.results["Missing Attendee"], set((
+            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+        )))
+        self.assertEqual(calverify.results["Mismatch Attendee"], set((
+            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+            ("MISMATCH3_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+        )))
+        self.assertEqual(calverify.results["Missing Organizer"], set((
+            ("MISSING_ORGANIZER_ICS", self.uuid2, self.uuid1,),
+            ("MISSING_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+        )))
+        self.assertEqual(calverify.results["Mismatch Organizer"], set((
+            ("MISMATCH_ORGANIZER_ICS", self.uuid2, self.uuid1,),
+            ("MISMATCH_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+            ("MISMATCH2_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+        )))
+
+        self.assertTrue("Fix change event" not in calverify.results)
+        self.assertTrue("Fix add event" not in calverify.results)
+        self.assertTrue("Fix add inbox" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)
+
+        sync_token_new1 = (yield (yield self.calendarUnderTest(self.uuid1)).syncToken())
+        sync_token_new2 = (yield (yield self.calendarUnderTest(self.uuid2)).syncToken())
+        sync_token_new3 = (yield (yield self.calendarUnderTest(self.uuid3)).syncToken())
+        self.assertEqual(sync_token_old1, sync_token_new1)
+        self.assertEqual(sync_token_old2, sync_token_new2)
+        self.assertEqual(sync_token_old3, sync_token_new3)
+
+
+    @inlineCallbacks
+    def test_fixMismatch(self):
+        """
+        CalVerifyService.doScan with fix for mismatches. Make sure it detects
+        and fixes as much as it can. Make sure sync-token is not changed.
+        """
+
+        sync_token_old1 = (yield (yield self.calendarUnderTest(self.uuid1)).syncToken())
+        sync_token_old2 = (yield (yield self.calendarUnderTest(self.uuid2)).syncToken())
+        sync_token_old3 = (yield (yield self.calendarUnderTest(self.uuid3)).syncToken())
+        self.commit()
+
+        options = {
+            "ical":False,
+            "badcua":False,
+            "mismatch":True,
+            "nobase64":False,
+            "verbose":False,
+            "details":False,
+            "uid":"",
+            "uuid":"",
+        }
+        output = StringIO()
+        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doScan(False, True, True, start=PyCalendarDateTime(2000, 1, 1, 0, 0, 0))
+
+        self.assertEqual(calverify.results["Number of events to process"], 15)
+        self.assertEqual(calverify.results["Missing Attendee"], set((
+            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISSING_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+        )))
+        self.assertEqual(calverify.results["Mismatch Attendee"], set((
+            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISMATCH_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid2,),
+            ("MISMATCH2_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+            ("MISMATCH3_ATTENDEE_ICS", self.uuid1, self.uuid3,),
+        )))
+        self.assertEqual(calverify.results["Missing Organizer"], set((
+            ("MISSING_ORGANIZER_ICS", self.uuid2, self.uuid1,),
+            ("MISSING_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+        )))
+        self.assertEqual(calverify.results["Mismatch Organizer"], set((
+            ("MISMATCH_ORGANIZER_ICS", self.uuid2, self.uuid1,),
+            ("MISMATCH_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+            ("MISMATCH2_ORGANIZER_ICS", self.uuid3, self.uuid1,),
+        )))
+        
+        self.assertEqual(calverify.results["Fix change event"], set((
+            (self.uuid2, "calendar", "MISMATCH_ATTENDEE_ICS",),
+            (self.uuid3, "calendar", "MISMATCH_ATTENDEE_ICS",),
+            (self.uuid2, "calendar", "MISMATCH2_ATTENDEE_ICS",),
+            (self.uuid3, "calendar2", "MISMATCH2_ATTENDEE_ICS",),
+            (self.uuid3, "calendar", "MISMATCH3_ATTENDEE_ICS",),
+            (self.uuid2, "calendar", "MISMATCH_ORGANIZER_ICS",),
+            (self.uuid3, "calendar2", "MISMATCH_ORGANIZER_ICS",),
+        )))
+        
+        self.assertEqual(calverify.results["Fix add event"], set((
+            (self.uuid2, "calendar", "MISSING_ATTENDEE_ICS",),
+            (self.uuid3, "calendar2", "MISSING_ATTENDEE_ICS",),
+        )))
+        
+        self.assertEqual(calverify.results["Fix add inbox"], set((
+            (self.uuid2, "MISSING_ATTENDEE_ICS",),
+            (self.uuid3, "MISSING_ATTENDEE_ICS",),
+            (self.uuid2, "MISMATCH_ATTENDEE_ICS",),
+            (self.uuid3, "MISMATCH_ATTENDEE_ICS",),
+            (self.uuid2, "MISMATCH2_ATTENDEE_ICS",),
+            (self.uuid3, "MISMATCH2_ATTENDEE_ICS",),
+            (self.uuid3, "MISMATCH3_ATTENDEE_ICS",),
+            (self.uuid2, "MISMATCH_ORGANIZER_ICS",),
+            (self.uuid3, "MISMATCH_ORGANIZER_ICS",),
+        )))
+        
+        self.assertEqual(calverify.results["Fix remove"], set((
+            (self.uuid2, "calendar", "missing_organizer.ics",),
+            (self.uuid3, "calendar", "missing_organizer.ics",),
+            (self.uuid3, "calendar", "mismatched2_organizer.ics",),
+        )))
+        obj = yield self.calendarObjectUnderTest(self.uuid2, "calendar", "missing_organizer.ics")
+        self.assertEqual(obj, None)
+        obj = yield self.calendarObjectUnderTest(self.uuid3, "calendar", "missing_organizer.ics")
+        self.assertEqual(obj, None)
+        obj = yield self.calendarObjectUnderTest(self.uuid3, "calendar", "mismatched2_organizer.ics")
+        self.assertEqual(obj, None)
+
+        sync_token_new1 = (yield (yield self.calendarUnderTest(self.uuid1)).syncToken())
+        sync_token_new2 = (yield (yield self.calendarUnderTest(self.uuid2)).syncToken())
+        sync_token_new3 = (yield (yield self.calendarUnderTest(self.uuid3)).syncToken())
+        self.assertEqual(sync_token_old1, sync_token_new1)
+        self.assertNotEqual(sync_token_old2, sync_token_new2)
+        self.assertNotEqual(sync_token_old3, sync_token_new3)
+
+        # Re-scan after changes to make sure there are no errors
+        self.commit()
+        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doScan(False, True, False, start=PyCalendarDateTime(2000, 1, 1, 0, 0, 0))
+
+        self.assertEqual(calverify.results["Number of events to process"], 14)
+        self.assertTrue("Missing Attendee" not in calverify.results)
+        self.assertTrue("Mismatch Attendee" not in calverify.results)
+        self.assertTrue("Missing Organizer" not in calverify.results)
+        self.assertTrue("Mismatch Organizer" not in calverify.results)
+        self.assertTrue("Fix add event" not in calverify.results)
+        self.assertTrue("Fix add inbox" not in calverify.results)
+        self.assertTrue("Fix remove" not in calverify.results)

Copied: CalendarServer/branches/users/gaya/sharedabgroups/contrib/tools/harpoon.py (from rev 9230, CalendarServer/trunk/contrib/tools/harpoon.py)
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/contrib/tools/harpoon.py	                        (rev 0)
+++ CalendarServer/branches/users/gaya/sharedabgroups/contrib/tools/harpoon.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -0,0 +1,62 @@
+#!/ngs/app/ical/code/bin/python
+
+##
+# Copyright (c) 2012 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.
+##
+
+# Sends SIGTERM to any calendar server child process whose VSIZE exceeds 2GB
+# Only for use in a specific environment
+
+import os
+import signal
+
+CUTOFFBYTES = 2 * 1024 * 1024 * 1024
+PROCDIR = "/proc"
+PYTHON = "/ngs/app/ical/code/bin/python"
+CMDARG = "LogID"
+
+serverProcessCount = 0
+numKilled = 0
+
+for pidString in sorted(os.listdir(PROCDIR)):
+
+    try:
+        pidNumber = int(pidString)
+    except ValueError:
+        # Not a process number
+        continue
+
+    pidDir = os.path.join(PROCDIR, pidString)
+    statsFile = os.path.join(pidDir, "stat")
+    statLine = open(statsFile).read()
+    stats = statLine.split()
+    vsize = int(stats[22])
+    cmdFile = os.path.join(pidDir, "cmdline")
+    if os.path.exists(cmdFile):
+        cmdLine = open(cmdFile).read().split('\x00')
+        if cmdLine[0].startswith(PYTHON):
+            for arg in cmdLine[1:]:
+                if arg.startswith(CMDARG):
+                    break
+            else:
+                continue
+            serverProcessCount += 1
+            if vsize > CUTOFFBYTES:
+                print "Killing process %d with VSIZE %d" % (pidNumber, vsize)
+                os.kill(pidNumber, signal.SIGTERM)
+                numKilled += 1
+
+print "Examined %d server processes" % (serverProcessCount,)
+print "Killed %d processes" % (numKilled,)

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/caldav/datastore/test/common.py	2012-05-04 21:53:03 UTC (rev 9230)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/caldav/datastore/test/common.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -78,13 +78,13 @@
     "4.ics",
 ]
 
-
 home1_calendarNames = [
     "calendar_1",
     "calendar_2",
     "calendar_empty",
 ]
 
+OTHER_HOME_UID = "home_splits"
 
 test_event_text = (
     "BEGIN:VCALENDAR\r\n"
@@ -990,9 +990,9 @@
         L{ICalendar.shareWith} will share a calendar with a given home UID.
         """
         cal = yield self.calendarUnderTest()
-        OTHER_HOME_UID = "home_splits"
         other = yield self.homeUnderTest(name=OTHER_HOME_UID)
         newCalName = yield cal.shareWith(other, _BIND_MODE_WRITE)
+        self.sharedName = newCalName
         yield self.commit()
         normalCal = yield self.calendarUnderTest()
         otherHome = yield self.homeUnderTest(name=OTHER_HOME_UID)
@@ -1021,11 +1021,13 @@
         """
         yield self.test_shareWith()
         # yield self.commit() # txn is none? why?
-        OTHER_HOME_UID = "home_splits"
         cal = yield self.calendarUnderTest()
         other = yield self.homeUnderTest(name=OTHER_HOME_UID)
         newName = yield cal.shareWith(other, _BIND_MODE_READ)
-        otherCal = yield other.sharedChildWithName(newName)
+        otherCal = yield other.sharedChildWithName(self.sharedName)
+
+        # Name should not change just because we updated the mode.
+        self.assertEqual(newName, self.sharedName)
         self.assertNotIdentical(otherCal, None)
 
         # FIXME: permission information should be visible on the retrieved
@@ -1036,6 +1038,38 @@
 
 
     @inlineCallbacks
+    def test_unshareWith(self, commit=False):
+        """
+        L{ICalendar.unshareWith} will remove a previously-shared calendar from
+        another user's calendar home.
+        """
+        # XXX: ideally this would actually be using the shared calendar object
+        # from the shareee's home and just calling .unshare() on it.
+        yield self.test_shareWith()
+        if commit:
+            yield self.commit()
+        cal = yield self.calendarUnderTest()
+        other = yield self.homeUnderTest(name=OTHER_HOME_UID)
+        newName = yield cal.unshareWith(other)
+        otherCal = yield other.sharedChildWithName(newName)
+        self.assertIdentical(otherCal, None)
+        invites = yield cal.retrieveOldInvites().allRecords()
+        self.assertEqual(len(invites), 0)
+        shares = yield other.retrieveOldShares().allRecords()
+        self.assertEqual(len(shares), 0)
+
+
+    @inlineCallbacks
+    def test_unshareWithInDifferentTransaction(self):
+        """
+        L{ICalendar.unshareWith} will remove a previously-shared calendar from
+        another user's calendar home, assuming the sharing was committed in a
+        previous transaction.
+        """
+        yield self.test_unshareWith(True)
+
+
+    @inlineCallbacks
     def test_hasCalendarResourceUIDSomewhereElse(self):
         """
         L{ICalendarHome.hasCalendarResourceUIDSomewhereElse} will determine if

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql.py	2012-05-04 21:53:03 UTC (rev 9230)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/sql.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -2028,6 +2028,31 @@
         returnValue(sharedName)
 
 
+    @inlineCallbacks
+    def unshareWith(self, shareeHome):
+        """
+        Remove the shared version of this (owned) L{CommonHomeChild} from the
+        referenced L{CommonHome}.
+
+        @see: L{CommonHomeChild.shareWith}
+
+        @param shareeHome: The home with which this L{CommonHomeChild} was
+            previously shared.
+
+        @return: a L{Deferred} which will fire with the previously-used name.
+        """
+        bind = self._bindSchema
+        resourceName = (yield Delete(
+            From=bind,
+            Where=(bind.RESOURCE_ID == Parameter("resourceID"))
+                  .And(bind.HOME_RESOURCE_ID == Parameter("homeID")),
+            Return=bind.RESOURCE_NAME,
+        ).on(self._txn, resourceID=self._resourceID,
+             homeID=shareeHome._resourceID))[0][0]
+        shareeHome._sharedChildren.pop(resourceName, None)
+        returnValue(resourceName)
+
+
     @classmethod
     @inlineCallbacks
     def loadAllObjects(cls, home, owned):

Modified: CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/test/util.py	2012-05-04 21:53:03 UTC (rev 9230)
+++ CalendarServer/branches/users/gaya/sharedabgroups/txdav/common/datastore/test/util.py	2012-05-07 18:12:03 UTC (rev 9231)
@@ -504,7 +504,7 @@
     def transactionUnderTest(self):
         """
         Create a transaction from C{storeUnderTest} and save it as
-        C[lastTransaction}.  Also makes sure to use the same store, saving the
+        C{lastTransaction}.  Also makes sure to use the same store, saving the
         value from C{storeUnderTest}.
         """
         if self.lastTransaction is None:
@@ -545,7 +545,7 @@
 
     def abort(self):
         """
-        Abort the last transaction created from C[transactionUnderTest}, and
+        Abort the last transaction created from C{transactionUnderTest}, and
         clear it.
         """
         result = self.lastTransaction.abort()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120507/b0705ee5/attachment-0001.html>


More information about the calendarserver-changes mailing list