[CalendarServer-changes] [10767] CalendarServer/trunk/calendarserver/tools

source_changes at macosforge.org source_changes at macosforge.org
Wed Feb 20 12:08:47 PST 2013


Revision: 10767
          http://trac.calendarserver.org//changeset/10767
Author:   cdaboo at apple.com
Date:     2013-02-20 12:08:47 -0800 (Wed, 20 Feb 2013)
Log Message:
-----------
Refactor calverify into multiple service classes to make adding new functionality easier and to potentially
allow the individual actions to be moved to store work queue items.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/calverify.py
    CalendarServer/trunk/calendarserver/tools/test/test_calverify.py

Modified: CalendarServer/trunk/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/calverify.py	2013-02-20 18:31:53 UTC (rev 10766)
+++ CalendarServer/trunk/calendarserver/tools/calverify.py	2013-02-20 20:08:47 UTC (rev 10767)
@@ -49,7 +49,7 @@
 from pycalendar.timezone import PyCalendarTimezone
 from twext.enterprise.dal.syntax import Select, Parameter, Count
 from twisted.application.service import Service
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python import log, usage
 from twisted.python.usage import Options
 from twistedcaldav import caldavxml
@@ -191,7 +191,7 @@
 if not hasattr(Component, "maxAlarmCounts"):
     Component.hasDuplicateAlarms = new_hasDuplicateAlarms
 
-VERSION = "8"
+VERSION = "9"
 
 def printusage(e=None):
     if e:
@@ -321,25 +321,9 @@
 
 class CalVerifyService(Service, object):
     """
-    Service which runs, exports the appropriate records, then stops the reactor.
+    Base class for common service behaviors.
     """
 
-    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
@@ -350,16 +334,9 @@
         self._directory = None
 
         self.cuaCache = {}
-        self.validForCalendaringUUIDs = {}
 
         self.results = {}
         self.summary = []
-        self.fixAttendeesForOrganizerMissing = 0
-        self.fixAttendeesForOrganizerMismatch = 0
-        self.fixOrganizersForAttendeeMissing = 0
-        self.fixOrganizersForAttendeeMismatch = 0
-        self.fixFailed = 0
-        self.fixedAutoAccepts = []
         self.total = 0
         self.totalErrors = None
         self.totalExceptions = None
@@ -373,25 +350,31 @@
         self.doCalVerify()
 
 
+    def stopService(self):
+        """
+        Stop the service.  Nothing to do; everything should be finished by this
+        time.
+        """
+        # TODO: stopping this service mid-export should really stop the export
+        # loop, but this is not implemented because nothing will actually do it
+        # except hitting ^C (which also calls reactor.stop(), so that will exit
+        # anyway).
+        pass
+
+
+    def title(self):
+        return ""
+
+
     @inlineCallbacks
     def doCalVerify(self):
         """
-        Do the export, stopping the reactor when done.
+        Do the operation stopping the reactor when done.
         """
-        self.output.write("\n---- CalVerify version: %s ----\n" % (VERSION,))
+        self.output.write("\n---- CalVerify %s version: %s ----\n" % (self.title(), VERSION,))
 
         try:
-            if self.options["nuke"]:
-                yield self.doNuke()
-            else:
-                if self.options["missing"]:
-                    yield self.doOrphans()
-
-                if self.options["mismatch"] or self.options["ical"] or self.options["badcua"]:
-                    yield self.doScan(self.options["ical"] or self.options["badcua"], self.options["mismatch"], self.options["fix"])
-
-                self.printSummary()
-
+            yield self.doAction()
             self.output.close()
         except:
             log.err()
@@ -399,148 +382,17 @@
         self.reactor.stop()
 
 
-    @inlineCallbacks
-    def doNuke(self):
+    def directoryService(self):
         """
-        Remove a resource using either its path or resource id. When doing this do not
-        read the iCalendar data which may be corrupt.
+        Get an appropriate directory service for this L{CalVerifyService}'s
+        configuration, creating one first if necessary.
         """
+        if self._directory is None:
+            self._directory = getDirectory(self.config) #directoryFromConfig(self.config)
+        return self._directory
 
-        self.output.write("\n---- Removing calendar resource ----\n")
-        self.txn = self.store.newTransaction()
 
-        nuke = self.options["nuke"]
-        if nuke.startswith("/calendars/__uids__/"):
-            pathbits = nuke.split("/")
-            if len(pathbits) != 6:
-                printusage("Not a valid calendar object resource path: %s" % (nuke,))
-            homeName = pathbits[3]
-            calendarName = pathbits[4]
-            resourceName = pathbits[5]
-
-            rid = yield self.getResourceID(homeName, calendarName, resourceName)
-            if rid is None:
-                yield self.txn.commit()
-                self.txn = None
-                self.output.write("\n")
-                self.output.write("Path does not exist. Nothing nuked.\n")
-                returnValue(None)
-            rid = int(rid)
-        else:
-            try:
-                rid = int(nuke)
-            except ValueError:
-                printusage("nuke argument must be a calendar object path or an SQL resource-id")
-
-        if self.options["fix"]:
-            result = yield self.fixByRemovingEvent(rid)
-            if result:
-                self.output.write("\n")
-                self.output.write("Removed resource: %s.\n" % (rid,))
-        else:
-            self.output.write("\n")
-            self.output.write("Resource: %s.\n" % (rid,))
-        yield self.txn.commit()
-        self.txn = None
-
-
     @inlineCallbacks
-    def doOrphans(self):
-        """
-        Report on home collections for which there are no directory records, or record is for user on
-        a different pod, or a user not enabled for calendaring.
-        """
-        self.output.write("\n---- Finding calendar homes with missing or disabled directory records ----\n")
-        self.txn = self.store.newTransaction()
-
-        if self.options["verbose"]:
-            t = time.time()
-        uids = yield self.getAllHomeUIDs()
-        if self.options["verbose"]:
-            self.output.write("getAllHomeUIDs time: %.1fs\n" % (time.time() - t,))
-        missing = []
-        wrong_server = []
-        disabled = []
-        uids_len = len(uids)
-        uids_div = 1 if uids_len < 100 else uids_len / 100
-        self.addToSummary("Total Homes", uids_len)
-
-        for ctr, uid in enumerate(uids):
-            if self.options["verbose"] and divmod(ctr, uids_div)[1] == 0:
-                self.output.write(("\r%d of %d (%d%%)" % (
-                    ctr + 1,
-                    uids_len,
-                    ((ctr + 1) * 100 / uids_len),
-                )).ljust(80))
-                self.output.flush()
-
-            record = self.directoryService().recordWithGUID(uid)
-            if record is None:
-                contents = yield self.countHomeContents(uid)
-                missing.append((uid, contents,))
-            elif not record.thisServer():
-                contents = yield self.countHomeContents(uid)
-                wrong_server.append((uid, contents,))
-            elif not record.enabledForCalendaring:
-                contents = yield self.countHomeContents(uid)
-                disabled.append((uid, contents,))
-
-            # To avoid holding locks on all the rows scanned, commit every 100 resources
-            if divmod(ctr, 100)[1] == 0:
-                yield self.txn.commit()
-                self.txn = self.store.newTransaction()
-
-        yield self.txn.commit()
-        self.txn = None
-        if self.options["verbose"]:
-            self.output.write("\r".ljust(80) + "\n")
-
-        # Print table of results
-        table = tables.Table()
-        table.addHeader(("Owner UID", "Calendar Objects"))
-        for uid, count in sorted(missing, key=lambda x: x[0]):
-            table.addRow((
-                uid,
-                count,
-            ))
-
-        self.output.write("\n")
-        self.output.write("Homes without a matching directory record (total=%d):\n" % (len(missing),))
-        table.printTable(os=self.output)
-        self.addToSummary("Homes without a matching directory record", len(missing), uids_len)
-
-        # Print table of results
-        table = tables.Table()
-        table.addHeader(("Owner UID", "Calendar Objects"))
-        for uid, count in sorted(wrong_server, key=lambda x: x[0]):
-            record = self.directoryService().recordWithGUID(uid)
-            table.addRow((
-                "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
-                count,
-            ))
-
-        self.output.write("\n")
-        self.output.write("Homes not hosted on this server (total=%d):\n" % (len(wrong_server),))
-        table.printTable(os=self.output)
-        self.addToSummary("Homes not hosted on this server", len(wrong_server), uids_len)
-
-        # Print table of results
-        table = tables.Table()
-        table.addHeader(("Owner UID", "Calendar Objects"))
-        for uid, count in sorted(disabled, key=lambda x: x[0]):
-            record = self.directoryService().recordWithGUID(uid)
-            table.addRow((
-                "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
-                count,
-            ))
-
-        self.output.write("\n")
-        self.output.write("Homes without an enabled directory record (total=%d):\n" % (len(disabled),))
-        table.printTable(os=self.output)
-        self.addToSummary("Homes without an enabled directory record", len(disabled), uids_len)
-
-
-    @inlineCallbacks
     def getAllHomeUIDs(self):
         ch = schema.CALENDAR_HOME
         rows = (yield Select(
@@ -568,102 +420,6 @@
 
 
     @inlineCallbacks
-    def doScan(self, ical, mismatch, fix, start=None):
-
-        self.output.write("\n---- Scanning calendar data ----\n")
-
-        self.now = PyCalendarDateTime.getNowUTC()
-        self.start = start if start is not None else PyCalendarDateTime.getToday()
-        self.start.setDateOnly(False)
-        self.end = self.start.duplicate()
-        self.end.offsetYear(1)
-        self.fix = fix
-
-        self.tzid = PyCalendarTimezone(tzid=self.options["tzid"] if self.options["tzid"] else "America/Los_Angeles")
-
-        self.txn = self.store.newTransaction()
-
-        if self.options["verbose"]:
-            t = time.time()
-        descriptor = None
-        if ical:
-            if self.options["uuid"]:
-                rows = yield self.getAllResourceInfoWithUUID(self.options["uuid"], inbox=True)
-                descriptor = "getAllResourceInfoWithUUID"
-            elif self.options["uid"]:
-                rows = yield self.getAllResourceInfoWithUID(self.options["uid"], inbox=True)
-                descriptor = "getAllResourceInfoWithUID"
-            else:
-                rows = yield self.getAllResourceInfo(inbox=True)
-                descriptor = "getAllResourceInfo"
-        else:
-            if self.options["uid"]:
-                rows = yield self.getAllResourceInfoWithUID(self.options["uid"])
-                descriptor = "getAllResourceInfoWithUID"
-            else:
-                rows = yield self.getAllResourceInfoTimeRange(self.start)
-                descriptor = "getAllResourceInfoTimeRange"
-
-        yield self.txn.commit()
-        self.txn = None
-
-        if self.options["verbose"]:
-            self.output.write("%s time: %.1fs\n" % (descriptor, time.time() - t,))
-
-        self.total = len(rows)
-        self.output.write("Number of events to process: %s\n" % (len(rows,)))
-        self.results["Number of events to process"] = len(rows)
-        self.addToSummary("Number of events to process", self.total)
-
-        # Split into organizer events and attendee events
-        self.organized = []
-        self.organized_byuid = {}
-        self.attended = []
-        self.attended_byuid = collections.defaultdict(list)
-        self.matched_attendee_to_organizer = collections.defaultdict(set)
-        skipped, inboxes = self.buildResourceInfo(rows)
-
-        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)
-        self.results["Number of attendee events to process"] = len(self.attended)
-        self.results["Number of skipped events"] = skipped
-        self.results["Number of inbox events"] = inboxes
-        self.addToSummary("Number of organizer events to process", len(self.organized), self.total)
-        self.addToSummary("Number of attendee events to process", len(self.attended), self.total)
-        self.addToSummary("Number of skipped events", skipped, self.total)
-        if ical:
-            self.addToSummary("Number of inbox events", inboxes, self.total)
-        self.addSummaryBreak()
-
-        if ical:
-            yield self.calendarDataCheck(rows)
-        elif mismatch:
-            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.results["Fixed Auto-Accepts"] = self.fixedAutoAccepts
-                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)
-
-                self.printAutoAccepts()
-
-        yield succeed(None)
-
-
-    @inlineCallbacks
     def getAllResourceInfo(self, inbox=False):
         co = schema.CALENDAR_OBJECT
         cb = schema.CALENDAR_BIND
@@ -737,7 +493,7 @@
                     cb.CALENDAR_RESOURCE_NAME != "inbox").And(
                     co.ORGANIZER != "")).join(
                 tr, type="left", on=(co.RESOURCE_ID == tr.CALENDAR_OBJECT_RESOURCE_ID)),
-            Where=(tr.START_DATE >= Parameter("Start")).Or(co.RECURRANCE_MAX == Parameter("Max")),
+            Where=(tr.START_DATE >= Parameter("Start")).Or(co.RECURRANCE_MAX <= Parameter("Start")),
             GroupBy=(ch.OWNER_UID, co.RESOURCE_ID, co.ICALENDAR_UID, cb.CALENDAR_RESOURCE_NAME, co.MD5, co.ORGANIZER, co.CREATED, co.MODIFIED,),
         ).on(self.txn, **kwds))
         returnValue(tuple(rows))
@@ -812,57 +568,409 @@
         returnValue(rows[0][0] if rows else None)
 
 
-    def buildResourceInfo(self, rows, onlyOrganizer=False, onlyAttendee=False):
-        skipped = 0
-        inboxes = 0
-        for owner, resid, uid, calname, md5, organizer, created, modified in rows:
+    @inlineCallbacks
+    def getCalendar(self, resid, doFix=False):
+        co = schema.CALENDAR_OBJECT
+        kwds = {"ResourceID" : resid}
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT],
+            From=co,
+            Where=(
+                co.RESOURCE_ID == Parameter("ResourceID")
+            ),
+        ).on(self.txn, **kwds))
+        try:
+            caldata = PyCalendar.parseText(rows[0][0]) if rows else None
+        except PyCalendarError:
+            caltxt = rows[0][0] if rows else None
+            if caltxt:
+                caltxt = caltxt.replace("\r\n ", "")
+                if caltxt.find("CALENDARSERVER-OLD-CUA=\"//") != -1:
+                    if doFix:
+                        caltxt = (yield self.fixBadOldCua(resid, caltxt))
+                        try:
+                            caldata = PyCalendar.parseText(caltxt) if rows else None
+                        except PyCalendarError:
+                            self.parseError = "No fix bad CALENDARSERVER-OLD-CUA"
+                            returnValue(None)
+                    else:
+                        self.parseError = "Bad CALENDARSERVER-OLD-CUA"
+                        returnValue(None)
 
-            # Skip owners not enabled for calendaring
-            if not self.testForCalendaringUUID(owner):
-                skipped += 1
-                continue
+            self.parseError = "Failed to parse"
+            returnValue(None)
 
-            # Skip inboxes
-            if calname == "inbox":
-                inboxes += 1
-                continue
+        self.parseError = None
+        returnValue(caldata)
 
-            # 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,))
+    @inlineCallbacks
+    def getCalendarForOwnerByUID(self, owner, uid):
+        co = schema.CALENDAR_OBJECT
+        cb = schema.CALENDAR_BIND
+        ch = schema.CALENDAR_HOME
 
-        return skipped, inboxes
+        kwds = {"OWNER": owner, "UID": uid}
+        rows = (yield Select(
+            [co.ICALENDAR_TEXT, co.RESOURCE_ID, co.CREATED, co.MODIFIED, ],
+            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, None, None,))
 
-    def testForCalendaringUUID(self, uuid):
+        returnValue((caldata, rows[0][1], rows[0][2], rows[0][3],) if rows else (None, None, None, None,))
+
+
+    @inlineCallbacks
+    def fixBadOldCua(self, resid, caltxt):
         """
-        Determine if the specified directory UUID is valid for calendaring. Keep a cache of
-        valid and invalid so we can do this quickly.
+        Fix bad CALENDARSERVER-OLD-CUA lines and write fixed data to store. Assumes iCalendar data lines unfolded.
+        """
 
-        @param uuid: the directory UUID to test
-        @type uuid: C{str}
+        # Get store objects
+        homeID, calendarID = yield self.getAllResourceInfoForResourceID(resid)
+        home = yield self.txn.calendarHomeWithResourceID(homeID)
+        calendar = yield home.childWithID(calendarID)
+        calendarObj = yield calendar.objectResourceWithID(resid)
 
-        @return: C{True} if valid, C{False} if not
+        # Do raw data fix one line at a time
+        caltxt = self.fixBadOldCuaLines(caltxt)
+
+        # Re-parse
+        try:
+            component = Component.fromString(caltxt)
+        except InvalidICalendarDataError:
+            returnValue(None)
+
+        # Write out fix, commit and get a new transaction
+        # Use _migrating to ignore possible overridden instance errors - we are either correcting or ignoring those
+        self.txn._migrating = True
+        component = yield calendarObj.setComponent(component)
+        yield self.txn.commit()
+        self.txn = self.store.newTransaction()
+
+        returnValue(caltxt)
+
+
+    def fixBadOldCuaLines(self, caltxt):
         """
+        Fix bad CALENDARSERVER-OLD-CUA lines. Assumes iCalendar data lines unfolded.
+        """
 
-        if uuid not in self.validForCalendaringUUIDs:
-            record = self.directoryService().recordWithGUID(uuid)
-            self.validForCalendaringUUIDs[uuid] = record is not None and record.enabledForCalendaring and record.thisServer()
-        return self.validForCalendaringUUIDs[uuid]
+        # Do raw data fix one line at a time
+        lines = caltxt.splitlines()
+        for ctr, line in enumerate(lines):
+            startpos = line.find(";CALENDARSERVER-OLD-CUA=\"//")
+            if startpos != -1:
+                endpos = line.find("urn:uuid:")
+                if endpos != -1:
+                    endpos += len("urn:uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"")
+                    badparam = line[startpos + len(";CALENDARSERVER-OLD-CUA=\""):endpos]
+                    endbadparam = badparam.find(";")
+                    if endbadparam != -1:
+                        badparam = badparam[:endbadparam].replace("\\", "")
+                        if badparam.find("8443") != -1:
+                            badparam = "https:" + badparam
+                        else:
+                            badparam = "http:" + badparam
+                        if self.options["nobase64"]:
+                            badparam = "\"" + badparam + "\""
+                        else:
+                            badparam = "base64-%s" % (base64.b64encode(badparam),)
+                        badparam = ";CALENDARSERVER-OLD-CUA=" + badparam
+                        lines[ctr] = line[:startpos] + badparam + line[endpos:]
+        caltxt = "\r\n".join(lines) + "\r\n"
+        return caltxt
 
 
     @inlineCallbacks
+    def removeEvent(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),
+        else:
+            percent = ""
+        self.summary.append((title, count, percent))
+
+
+    def addSummaryBreak(self):
+        self.summary.append(None)
+
+
+    def printSummary(self):
+        # Print summary of results
+        table = tables.Table()
+        table.addHeader(("Item", "Count", "%"))
+        table.setDefaultColumnFormats(
+            (
+                tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
+                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+                tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
+            )
+        )
+        for item in self.summary:
+            table.addRow(item)
+
+        if self.totalErrors is not None:
+            table.addRow(None)
+            table.addRow(("Total Errors", self.totalErrors, safePercent(self.totalErrors, self.total),))
+
+        self.output.write("\n")
+        self.output.write("Overall Summary:\n")
+        table.printTable(os=self.output)
+
+
+
+class NukeService(CalVerifyService):
+    """
+    Service which removes specific events.
+    """
+
+    def title(self):
+        return "Nuke Service"
+
+
+    @inlineCallbacks
+    def doAction(self):
+        """
+        Remove a resource using either its path or resource id. When doing this do not
+        read the iCalendar data which may be corrupt.
+        """
+
+        self.output.write("\n---- Removing calendar resource ----\n")
+        self.txn = self.store.newTransaction()
+
+        nuke = self.options["nuke"]
+        if nuke.startswith("/calendars/__uids__/"):
+            pathbits = nuke.split("/")
+            if len(pathbits) != 6:
+                printusage("Not a valid calendar object resource path: %s" % (nuke,))
+            homeName = pathbits[3]
+            calendarName = pathbits[4]
+            resourceName = pathbits[5]
+
+            rid = yield self.getResourceID(homeName, calendarName, resourceName)
+            if rid is None:
+                yield self.txn.commit()
+                self.txn = None
+                self.output.write("\n")
+                self.output.write("Path does not exist. Nothing nuked.\n")
+                returnValue(None)
+            rid = int(rid)
+        else:
+            try:
+                rid = int(nuke)
+            except ValueError:
+                printusage("nuke argument must be a calendar object path or an SQL resource-id")
+
+        if self.options["fix"]:
+            result = yield self.removeEvent(rid)
+            if result:
+                self.output.write("\n")
+                self.output.write("Removed resource: %s.\n" % (rid,))
+        else:
+            self.output.write("\n")
+            self.output.write("Resource: %s.\n" % (rid,))
+        yield self.txn.commit()
+        self.txn = None
+
+
+
+class OrphansService(CalVerifyService):
+    """
+    Service which removes specific events.
+    """
+
+    def title(self):
+        return "Orphans Service"
+
+
+    @inlineCallbacks
+    def doAction(self):
+        """
+        Report on home collections for which there are no directory records, or record is for user on
+        a different pod, or a user not enabled for calendaring.
+        """
+        self.output.write("\n---- Finding calendar homes with missing or disabled directory records ----\n")
+        self.txn = self.store.newTransaction()
+
+        if self.options["verbose"]:
+            t = time.time()
+        uids = yield self.getAllHomeUIDs()
+        if self.options["verbose"]:
+            self.output.write("getAllHomeUIDs time: %.1fs\n" % (time.time() - t,))
+        missing = []
+        wrong_server = []
+        disabled = []
+        uids_len = len(uids)
+        uids_div = 1 if uids_len < 100 else uids_len / 100
+        self.addToSummary("Total Homes", uids_len)
+
+        for ctr, uid in enumerate(uids):
+            if self.options["verbose"] and divmod(ctr, uids_div)[1] == 0:
+                self.output.write(("\r%d of %d (%d%%)" % (
+                    ctr + 1,
+                    uids_len,
+                    ((ctr + 1) * 100 / uids_len),
+                )).ljust(80))
+                self.output.flush()
+
+            record = self.directoryService().recordWithGUID(uid)
+            if record is None:
+                contents = yield self.countHomeContents(uid)
+                missing.append((uid, contents,))
+            elif not record.thisServer():
+                contents = yield self.countHomeContents(uid)
+                wrong_server.append((uid, contents,))
+            elif not record.enabledForCalendaring:
+                contents = yield self.countHomeContents(uid)
+                disabled.append((uid, contents,))
+
+            # To avoid holding locks on all the rows scanned, commit every 100 resources
+            if divmod(ctr, 100)[1] == 0:
+                yield self.txn.commit()
+                self.txn = self.store.newTransaction()
+
+        yield self.txn.commit()
+        self.txn = None
+        if self.options["verbose"]:
+            self.output.write("\r".ljust(80) + "\n")
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Calendar Objects"))
+        for uid, count in sorted(missing, key=lambda x: x[0]):
+            table.addRow((
+                uid,
+                count,
+            ))
+
+        self.output.write("\n")
+        self.output.write("Homes without a matching directory record (total=%d):\n" % (len(missing),))
+        table.printTable(os=self.output)
+        self.addToSummary("Homes without a matching directory record", len(missing), uids_len)
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Calendar Objects"))
+        for uid, count in sorted(wrong_server, key=lambda x: x[0]):
+            record = self.directoryService().recordWithGUID(uid)
+            table.addRow((
+                "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
+                count,
+            ))
+
+        self.output.write("\n")
+        self.output.write("Homes not hosted on this server (total=%d):\n" % (len(wrong_server),))
+        table.printTable(os=self.output)
+        self.addToSummary("Homes not hosted on this server", len(wrong_server), uids_len)
+
+        # Print table of results
+        table = tables.Table()
+        table.addHeader(("Owner UID", "Calendar Objects"))
+        for uid, count in sorted(disabled, key=lambda x: x[0]):
+            record = self.directoryService().recordWithGUID(uid)
+            table.addRow((
+                "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
+                count,
+            ))
+
+        self.output.write("\n")
+        self.output.write("Homes without an enabled directory record (total=%d):\n" % (len(disabled),))
+        table.printTable(os=self.output)
+        self.addToSummary("Homes without an enabled directory record", len(disabled), uids_len)
+
+        self.printSummary()
+
+
+
+class BadDataService(CalVerifyService):
+    """
+    Service which scans for bad calendar data.
+    """
+
+    def title(self):
+        return "Bad Data Service"
+
+
+    @inlineCallbacks
+    def doAction(self):
+
+        self.output.write("\n---- Scanning calendar data ----\n")
+
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.start = PyCalendarDateTime.getToday()
+        self.start.setDateOnly(False)
+        self.end = self.start.duplicate()
+        self.end.offsetYear(1)
+        self.fix = self.options["fix"]
+
+        self.tzid = PyCalendarTimezone(tzid=self.options["tzid"] if self.options["tzid"] else "America/Los_Angeles")
+
+        self.txn = self.store.newTransaction()
+
+        if self.options["verbose"]:
+            t = time.time()
+        descriptor = None
+        if self.options["uuid"]:
+            rows = yield self.getAllResourceInfoWithUUID(self.options["uuid"], inbox=True)
+            descriptor = "getAllResourceInfoWithUUID"
+        elif self.options["uid"]:
+            rows = yield self.getAllResourceInfoWithUID(self.options["uid"], inbox=True)
+            descriptor = "getAllResourceInfoWithUID"
+        else:
+            rows = yield self.getAllResourceInfo(inbox=True)
+            descriptor = "getAllResourceInfo"
+
+        yield self.txn.commit()
+        self.txn = None
+
+        if self.options["verbose"]:
+            self.output.write("%s time: %.1fs\n" % (descriptor, time.time() - t,))
+
+        self.total = len(rows)
+        self.output.write("Number of events to process: %s\n" % (len(rows,)))
+        self.results["Number of events to process"] = len(rows)
+        self.addToSummary("Number of events to process", self.total)
+        self.addSummaryBreak()
+
+        yield self.calendarDataCheck(rows)
+
+        self.printSummary()
+
+
+    @inlineCallbacks
     def calendarDataCheck(self, rows):
         """
         Check each calendar resource for valid iCalendar data.
@@ -1132,66 +1240,181 @@
         returnValue((result, message,))
 
 
+
+class SchedulingMismatchService(CalVerifyService):
+    """
+    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(SchedulingMismatchService, self).__init__(store, options, output, reactor, config)
+
+        self.validForCalendaringUUIDs = {}
+
+        self.fixAttendeesForOrganizerMissing = 0
+        self.fixAttendeesForOrganizerMismatch = 0
+        self.fixOrganizersForAttendeeMissing = 0
+        self.fixOrganizersForAttendeeMismatch = 0
+        self.fixFailed = 0
+        self.fixedAutoAccepts = []
+
+
     @inlineCallbacks
-    def fixBadOldCua(self, resid, caltxt):
+    def doAction(self):
+
+        self.output.write("\n---- Scanning calendar data ----\n")
+
+        self.now = PyCalendarDateTime.getNowUTC()
+        self.start = self.options["start"] if "start" in self.options else PyCalendarDateTime.getToday()
+        self.start.setDateOnly(False)
+        self.end = self.start.duplicate()
+        self.end.offsetYear(1)
+        self.fix = self.options["fix"]
+
+        self.tzid = PyCalendarTimezone(tzid=self.options["tzid"] if self.options["tzid"] else "America/Los_Angeles")
+
+        self.txn = self.store.newTransaction()
+
+        if self.options["verbose"]:
+            t = time.time()
+        descriptor = None
+        if self.options["uid"]:
+            rows = yield self.getAllResourceInfoWithUID(self.options["uid"])
+            descriptor = "getAllResourceInfoWithUID"
+        else:
+            rows = yield self.getAllResourceInfoTimeRange(self.start)
+            descriptor = "getAllResourceInfoTimeRange"
+
+        yield self.txn.commit()
+        self.txn = None
+
+        if self.options["verbose"]:
+            self.output.write("%s time: %.1fs\n" % (descriptor, time.time() - t,))
+
+        self.total = len(rows)
+        self.output.write("Number of events to process: %s\n" % (len(rows,)))
+        self.results["Number of events to process"] = len(rows)
+        self.addToSummary("Number of events to process", self.total)
+
+        # Split into organizer events and attendee events
+        self.organized = []
+        self.organized_byuid = {}
+        self.attended = []
+        self.attended_byuid = collections.defaultdict(list)
+        self.matched_attendee_to_organizer = collections.defaultdict(set)
+        skipped, inboxes = self.buildResourceInfo(rows)
+
+        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)
+        self.results["Number of attendee events to process"] = len(self.attended)
+        self.results["Number of skipped events"] = skipped
+        self.results["Number of inbox events"] = inboxes
+        self.addToSummary("Number of organizer events to process", len(self.organized), self.total)
+        self.addToSummary("Number of attendee events to process", len(self.attended), self.total)
+        self.addToSummary("Number of skipped events", skipped, self.total)
+        self.addSummaryBreak()
+
+        self.totalErrors = 0
+        yield self.verifyAllAttendeesForOrganizer()
+        yield self.verifyAllOrganizersForAttendee()
+
+        # Need to add fix summary information
+        if self.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.results["Fixed Auto-Accepts"] = self.fixedAutoAccepts
+            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)
+
+            self.printAutoAccepts()
+
+        self.printSummary()
+
+
+    def buildResourceInfo(self, rows, onlyOrganizer=False, onlyAttendee=False):
         """
-        Fix bad CALENDARSERVER-OLD-CUA lines and write fixed data to store. Assumes iCalendar data lines unfolded.
+        For each resource, determine whether it is an organizer or attendee event, and also
+        cache the attendee partstats.
+
+        @param rows: set of DB query rows
+        @type rows: C{list}
+        @param onlyOrganizer: whether organizer information only is required
+        @type onlyOrganizer: C{bool}
+        @param onlyAttendee: whether attendee information only is required
+        @type onlyAttendee: C{bool}
         """
 
-        # Get store objects
-        homeID, calendarID = yield self.getAllResourceInfoForResourceID(resid)
-        home = yield self.txn.calendarHomeWithResourceID(homeID)
-        calendar = yield home.childWithID(calendarID)
-        calendarObj = yield calendar.objectResourceWithID(resid)
+        skipped = 0
+        inboxes = 0
+        for owner, resid, uid, calname, md5, organizer, created, modified in rows:
 
-        # Do raw data fix one line at a time
-        caltxt = self.fixBadOldCuaLines(caltxt)
+            # Skip owners not enabled for calendaring
+            if not self.testForCalendaringUUID(owner):
+                skipped += 1
+                continue
 
-        # Re-parse
-        try:
-            component = Component.fromString(caltxt)
-        except InvalidICalendarDataError:
-            returnValue(None)
+            # Skip inboxes
+            if calname == "inbox":
+                inboxes += 1
+                continue
 
-        # Write out fix, commit and get a new transaction
-        # Use _migrating to ignore possible overridden instance errors - we are either correcting or ignoring those
-        self.txn._migrating = True
-        component = yield calendarObj.setComponent(component)
-        yield self.txn.commit()
-        self.txn = self.store.newTransaction()
+            # 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
 
-        returnValue(caltxt)
+            # 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 fixBadOldCuaLines(self, caltxt):
+
+    def testForCalendaringUUID(self, uuid):
         """
-        Fix bad CALENDARSERVER-OLD-CUA lines. Assumes iCalendar data lines unfolded.
+        Determine if the specified directory UUID is valid for calendaring. Keep a cache of
+        valid and invalid so we can do this quickly.
+
+        @param uuid: the directory UUID to test
+        @type uuid: C{str}
+
+        @return: C{True} if valid, C{False} if not
         """
 
-        # Do raw data fix one line at a time
-        lines = caltxt.splitlines()
-        for ctr, line in enumerate(lines):
-            startpos = line.find(";CALENDARSERVER-OLD-CUA=\"//")
-            if startpos != -1:
-                endpos = line.find("urn:uuid:")
-                if endpos != -1:
-                    endpos += len("urn:uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\"")
-                    badparam = line[startpos + len(";CALENDARSERVER-OLD-CUA=\""):endpos]
-                    endbadparam = badparam.find(";")
-                    if endbadparam != -1:
-                        badparam = badparam[:endbadparam].replace("\\", "")
-                        if badparam.find("8443") != -1:
-                            badparam = "https:" + badparam
-                        else:
-                            badparam = "http:" + badparam
-                        if self.options["nobase64"]:
-                            badparam = "\"" + badparam + "\""
-                        else:
-                            badparam = "base64-%s" % (base64.b64encode(badparam),)
-                        badparam = ";CALENDARSERVER-OLD-CUA=" + badparam
-                        lines[ctr] = line[:startpos] + badparam + line[endpos:]
-        caltxt = "\r\n".join(lines) + "\r\n"
-        return caltxt
+        if uuid not in self.validForCalendaringUUIDs:
+            record = self.directoryService().recordWithGUID(uuid)
+            self.validForCalendaringUUIDs[uuid] = record is not None and record.enabledForCalendaring and record.thisServer()
+        return self.validForCalendaringUUIDs[uuid]
 
 
     @inlineCallbacks
@@ -1452,7 +1675,7 @@
                 # If there is a miss we fix by removing the attendee data
                 if self.fix:
                     # This is where we attempt a fix
-                    fix_result = (yield self.fixByRemovingEvent(resid))
+                    fix_result = (yield self.removeEvent(resid))
                     if fix_result:
                         self.fixOrganizersForAttendeeMissing += 1
                     else:
@@ -1556,7 +1779,7 @@
             # 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)
+                yield self.removeEvent(attresid)
                 returnValue(True)
 
             # Convert iTip message into actual calendar data - just remove METHOD
@@ -1647,66 +1870,6 @@
                 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),
-        else:
-            percent = ""
-        self.summary.append((title, count, percent))
-
-
-    def addSummaryBreak(self):
-        self.summary.append(None)
-
-
-    def printSummary(self):
-        # Print summary of results
-        table = tables.Table()
-        table.addHeader(("Item", "Count", "%"))
-        table.setDefaultColumnFormats(
-            (
-                tables.Table.ColumnFormat("%s", tables.Table.ColumnFormat.LEFT_JUSTIFY),
-                tables.Table.ColumnFormat("%d", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-                tables.Table.ColumnFormat("%.1f%%", tables.Table.ColumnFormat.RIGHT_JUSTIFY),
-            )
-        )
-        for item in self.summary:
-            table.addRow(item)
-
-        if self.totalErrors is not None:
-            table.addRow(None)
-            table.addRow(("Total Errors", self.totalErrors, safePercent(self.totalErrors, self.total),))
-
-        self.output.write("\n")
-        self.output.write("Overall Summary:\n")
-        table.printTable(os=self.output)
-
-
     def printAutoAccepts(self):
         # Print summary of results
         table = tables.Table()
@@ -1725,71 +1888,11 @@
         table.printTable(os=self.output)
 
 
-    @inlineCallbacks
-    def getCalendar(self, resid, doFix=False):
-        co = schema.CALENDAR_OBJECT
-        kwds = {"ResourceID" : resid}
-        rows = (yield Select(
-            [co.ICALENDAR_TEXT],
-            From=co,
-            Where=(
-                co.RESOURCE_ID == Parameter("ResourceID")
-            ),
-        ).on(self.txn, **kwds))
-        try:
-            caldata = PyCalendar.parseText(rows[0][0]) if rows else None
-        except PyCalendarError:
-            caltxt = rows[0][0] if rows else None
-            if caltxt:
-                caltxt = caltxt.replace("\r\n ", "")
-                if caltxt.find("CALENDARSERVER-OLD-CUA=\"//") != -1:
-                    if doFix:
-                        caltxt = (yield self.fixBadOldCua(resid, caltxt))
-                        try:
-                            caldata = PyCalendar.parseText(caltxt) if rows else None
-                        except PyCalendarError:
-                            self.parseError = "No fix bad CALENDARSERVER-OLD-CUA"
-                            returnValue(None)
-                    else:
-                        self.parseError = "Bad CALENDARSERVER-OLD-CUA"
-                        returnValue(None)
-
-            self.parseError = "Failed to parse"
-            returnValue(None)
-
-        self.parseError = None
-        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, co.CREATED, co.MODIFIED, ],
-            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, None, None,))
-
-        returnValue((caldata, rows[0][1], rows[0][2], rows[0][3],) if rows else (None, None, None, None,))
-
-
     def masterComponent(self, calendar):
         """
         Return the master iCal component in this calendar.
-        @return: the L{Component} for the master component,
+
+        @return: the L{PyCalendarComponent} for the master component,
             or C{None} if there isn't one.
         """
         for component in calendar.getComponents(definitions.cICalComponent_VEVENT):
@@ -1882,32 +1985,9 @@
             component.replaceProperty(Property("TRANSP", "TRANSPARENT" if addTransp else "OPAQUE"))
 
 
-    def directoryService(self):
-        """
-        Get an appropriate directory service for this L{CalVerifyService}'s
-        configuration, creating one first if necessary.
-        """
-        if self._directory is None:
-            self._directory = getDirectory(self.config) #directoryFromConfig(self.config)
-        return self._directory
 
+def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
 
-    def stopService(self):
-        """
-        Stop the service.  Nothing to do; everything should be finished by this
-        time.
-        """
-        # TODO: stopping this service mid-export should really stop the export
-        # loop, but this is not implemented because nothing will actually do it
-        # except hitting ^C (which also calls reactor.stop(), so that will exit
-        # anyway).
-
-
-
-def main(argv=sys.argv, stderr=sys.stderr, reactor=None):
-    """
-    Do the export.
-    """
     if reactor is None:
         from twisted.internet import reactor
     options = CalVerifyOptions()
@@ -1926,9 +2006,16 @@
     def makeService(store):
         from twistedcaldav.config import config
         config.TransactionTimeoutSeconds = 0
-        return CalVerifyService(store, options, output, reactor, config)
+        if options["nuke"]:
+            return NukeService(store, options, output, reactor, config)
+        elif options["missing"]:
+            return OrphansService(store, options, output, reactor, config)
+        elif options["ical"] or options["badcua"]:
+            return BadDataService(store, options, output, reactor, config)
+        elif options["mismatch"]:
+            return SchedulingMismatchService(store, options, output, reactor, config)
 
-    utilityMain(options['config'], makeService, reactor, verbose=options['debug'])
+    utilityMain(options['config'], makeService, reactor)
 
 if __name__ == '__main__':
     main()

Modified: CalendarServer/trunk/calendarserver/tools/test/test_calverify.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_calverify.py	2013-02-20 18:31:53 UTC (rev 10766)
+++ CalendarServer/trunk/calendarserver/tools/test/test_calverify.py	2013-02-20 20:08:47 UTC (rev 10767)
@@ -20,7 +20,8 @@
 
 from StringIO import StringIO
 from calendarserver.tap.util import getRootResource
-from calendarserver.tools.calverify import CalVerifyService
+from calendarserver.tools.calverify import BadDataService, \
+    SchedulingMismatchService
 from pycalendar.datetime import PyCalendarDateTime
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
@@ -530,6 +531,7 @@
 
         options = {
             "ical": True,
+            "fix": False,
             "nobase64": False,
             "verbose": False,
             "uid": "",
@@ -537,9 +539,9 @@
             "tzid": "",
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, False)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -572,6 +574,7 @@
 
         options = {
             "ical": True,
+            "fix": True,
             "nobase64": False,
             "verbose": False,
             "uid": "",
@@ -583,9 +586,9 @@
         # Do fix
         self.patch(config.Scheduling.Options, "PrincipalHostAliases", "demo.com")
         self.patch(config, "HTTPPort", 8008)
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, True)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -603,9 +606,10 @@
         )))
 
         # Do scan
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        options["fix"] = False
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, False)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -640,6 +644,7 @@
 
         options = {
             "ical": False,
+            "fix": False,
             "badcua": True,
             "nobase64": False,
             "verbose": False,
@@ -648,9 +653,9 @@
             "tzid": "",
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, False)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -679,6 +684,7 @@
 
         options = {
             "ical": False,
+            "fix": True,
             "badcua": True,
             "nobase64": False,
             "verbose": False,
@@ -691,9 +697,9 @@
         # Do fix
         self.patch(config.Scheduling.Options, "PrincipalHostAliases", "demo.com")
         self.patch(config, "HTTPPort", 8008)
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, True)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -707,9 +713,10 @@
         )))
 
         # Do scan
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
+        options["fix"] = False
+        calverify = BadDataService(self._sqlCalendarStore, options, output, reactor, config)
         calverify.emailDomain = "example.com"
-        yield calverify.doScan(True, False, False)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 13)
         self.verifyResultsByUID(calverify.results["Bad iCalendar data"], set((
@@ -907,7 +914,7 @@
             "uuid": "",
             "tzid": "",
         }
-        calverifyNo64 = CalVerifyService(self._sqlCalendarStore, optionsNo64, StringIO(), reactor, config)
+        calverifyNo64 = BadDataService(self._sqlCalendarStore, optionsNo64, StringIO(), reactor, config)
         calverifyNo64.emailDomain = "example.com"
 
         options64 = {
@@ -918,7 +925,7 @@
             "uuid": "",
             "tzid": "",
         }
-        calverify64 = CalVerifyService(self._sqlCalendarStore, options64, StringIO(), reactor, config)
+        calverify64 = BadDataService(self._sqlCalendarStore, options64, StringIO(), reactor, config)
         calverify64.emailDomain = "example.com"
 
         for bad, oknobase64, okbase64 in data:
@@ -1467,15 +1474,17 @@
             "badcua": False,
             "mismatch": True,
             "nobase64": False,
+            "fix": False,
             "verbose": False,
             "details": False,
             "uid": "",
             "uuid": "",
             "tzid": "",
+            "start": PyCalendarDateTime(now, 1, 1, 0, 0, 0),
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
-        yield calverify.doScan(False, True, False, start=PyCalendarDateTime(now, 1, 1, 0, 0, 0))
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 17)
         self.assertEqual(calverify.results["Missing Attendee"], set((
@@ -1532,15 +1541,17 @@
             "badcua": False,
             "mismatch": True,
             "nobase64": False,
+            "fix": True,
             "verbose": False,
             "details": False,
             "uid": "",
             "uuid": "",
             "tzid": "",
+            "start": PyCalendarDateTime(now, 1, 1, 0, 0, 0),
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
-        yield calverify.doScan(False, True, True, start=PyCalendarDateTime(now, 1, 1, 0, 0, 0))
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 17)
         self.assertEqual(calverify.results["Missing Attendee"], set((
@@ -1615,8 +1626,9 @@
 
         # 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(now, 1, 1, 0, 0, 0))
+        options["fix"] = False
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 14)
         self.assertTrue("Missing Attendee" not in calverify.results)
@@ -1738,15 +1750,17 @@
             "badcua": False,
             "mismatch": True,
             "nobase64": False,
+            "fix": False,
             "verbose": False,
             "details": False,
             "uid": "",
             "uuid": "",
             "tzid": "",
+            "start": PyCalendarDateTime(now, 1, 1, 0, 0, 0),
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
-        yield calverify.doScan(False, True, False, start=PyCalendarDateTime(now, 1, 1, 0, 0, 0))
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 3)
         self.assertEqual(calverify.results["Missing Attendee"], set((
@@ -1787,15 +1801,17 @@
             "badcua": False,
             "mismatch": True,
             "nobase64": False,
+            "fix": True,
             "verbose": False,
             "details": False,
             "uid": "",
             "uuid": "",
             "tzid": "",
+            "start": PyCalendarDateTime(now, 1, 1, 0, 0, 0),
         }
         output = StringIO()
-        calverify = CalVerifyService(self._sqlCalendarStore, options, output, reactor, config)
-        yield calverify.doScan(False, True, True, start=PyCalendarDateTime(now, 1, 1, 0, 0, 0))
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 3)
         self.assertEqual(calverify.results["Missing Attendee"], set((
@@ -1837,8 +1853,9 @@
 
         # 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(now, 1, 1, 0, 0, 0))
+        options["fix"] = False
+        calverify = SchedulingMismatchService(self._sqlCalendarStore, options, output, reactor, config)
+        yield calverify.doAction()
 
         self.assertEqual(calverify.results["Number of events to process"], 4)
         self.assertTrue("Missing Attendee" not in calverify.results)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130220/33d190db/attachment-0001.html>


More information about the calendarserver-changes mailing list