From 0f39c12842d97634136ccaf6bd4a2f7aee49a59b Mon Sep 17 00:00:00 2001 From: Morgen Sagen Date: Sat, 27 Mar 2010 04:09:15 +0000 Subject: [PATCH] Adds the event-deletion portion of user deprovisioning. git-svn-id: https://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk@5408 e27351fd-9f3e-4f54-a53b-843176b1656c --- bin/calendarserver_purge_events | 4 +- calendarserver/tools/purge.py | 108 ++- .../tools/test/deprovision/augments.xml | 32 + .../tools/test/deprovision/caldavd.plist | 767 ++++++++++++++++++ .../test/deprovision/resources-locations.xml | 34 + .../tools/test/deprovision/users-groups.xml | 38 + calendarserver/tools/test/test_purge.py | 254 +++++- twistedcaldav/test/util.py | 37 +- 8 files changed, 1240 insertions(+), 34 deletions(-) create mode 100644 calendarserver/tools/test/deprovision/augments.xml create mode 100644 calendarserver/tools/test/deprovision/caldavd.plist create mode 100644 calendarserver/tools/test/deprovision/resources-locations.xml create mode 100644 calendarserver/tools/test/deprovision/users-groups.xml diff --git a/bin/calendarserver_purge_events b/bin/calendarserver_purge_events index 76e3c6d58..c0b72d1b2 100755 --- a/bin/calendarserver_purge_events +++ b/bin/calendarserver_purge_events @@ -40,5 +40,5 @@ if __name__ == "__main__": sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")] - from calendarserver.tools.purge import main - main() + from calendarserver.tools.purge import main_purge + main_purge() diff --git a/calendarserver/tools/purge.py b/calendarserver/tools/purge.py index 3accf1967..9fc1129b0 100755 --- a/calendarserver/tools/purge.py +++ b/calendarserver/tools/purge.py @@ -16,28 +16,29 @@ # limitations under the License. ## -from pwd import getpwnam -from twisted.python.util import switchUID -from twistedcaldav.directory.directory import DirectoryError -from grp import getgrnam from calendarserver.tap.util import FakeRequest from calendarserver.tap.util import getRootResource from calendarserver.tools.util import loadConfig, setupMemcached, setupNotifications -from datetime import date, timedelta +from datetime import date, timedelta, datetime from getopt import getopt, GetoptError +from grp import getgrnam +from pwd import getpwnam from twext.python.log import Logger +from twext.web2.dav import davxml from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.python.util import switchUID from twistedcaldav import caldavxml from twistedcaldav.caldavxml import TimeRange from twistedcaldav.config import config, ConfigurationError +from twistedcaldav.directory.directory import DirectoryError from twistedcaldav.method.delete_common import DeleteResource import os import sys log = Logger() -def usage(e=None): +def usage_purge(e=None): name = os.path.basename(sys.argv[0]) print "usage: %s [options]" % (name,) @@ -58,7 +59,7 @@ def usage(e=None): sys.exit(0) -def main(): +def main_purge(): try: (optargs, args) = getopt( @@ -71,7 +72,7 @@ def main(): ], ) except GetoptError, e: - usage(e) + usage_purge(e) # # Get configuration @@ -83,14 +84,14 @@ def main(): for opt, arg in optargs: if opt in ("-h", "--help"): - usage() + usage_purge() elif opt in ("-d", "--days"): try: days = int(arg) except ValueError, e: print "Invalid value for --days: %s" % (arg,) - usage(e) + usage_purge(e) elif opt in ("-v", "--verbose"): verbose = True @@ -104,6 +105,8 @@ def main(): else: raise NotImplementedError(opt) + cutoff = (date.today()-timedelta(days=days)).strftime("%Y%m%dT000000Z") + try: loadConfig(configFileName) @@ -127,22 +130,20 @@ def main(): print "Error: %s" % (e,) return - cutoff = (date.today() - timedelta(days=days)).strftime("%Y%m%dT000000Z") # # Start the reactor # - reactor.callLater(0.1, purgeThenStop, directory, rootResource, cutoff, - verbose=verbose, dryrun=dryrun) + reactor.callLater(0.1, callThenStop, purgeOldEvents, directory, + rootResource, cutoff, verbose=verbose, dryrun=dryrun) reactor.run() @inlineCallbacks -def purgeThenStop(directory, rootResource, cutoff, verbose=False, dryrun=False): +def callThenStop(method, *args, **kwds): try: - count = (yield purgeOldEvents(directory, rootResource, cutoff, - verbose=verbose, dryrun=dryrun)) - if dryrun: + count = (yield method(*args, **kwds)) + if kwds.get("dryrun", False): print "Would have purged %d events" % (count,) else: print "Purged %d events" % (count,) @@ -165,6 +166,9 @@ def purgeOldEvents(directory, root, date, verbose=False, dryrun=False): print "Scanning calendar homes ...", records = [] + calendars = root.getChild("calendars") + uidsFPath = calendars.fp.child("__uids__") + if uidsFPath.exists(): for firstFPath in uidsFPath.children(): if len(firstFPath.basename()) == 2: @@ -175,6 +179,7 @@ def purgeOldEvents(directory, root, date, verbose=False, dryrun=False): record = directory.recordWithUID(uid) if record is not None: records.append(record) + if verbose: print "%d calendar homes found" % (len(records),) @@ -224,7 +229,8 @@ def purgeOldEvents(directory, root, date, verbose=False, dryrun=False): ) try: if not dryrun: - (yield deleteResource(root, collection, resource, uri)) + (yield deleteResource(root, collection, resource, + uri, record.guid)) eventCount += 1 homeEventCount += 1 except Exception, e: @@ -237,12 +243,74 @@ def purgeOldEvents(directory, root, date, verbose=False, dryrun=False): returnValue(eventCount) -def deleteResource(root, collection, resource, uri): +def deleteResource(root, collection, resource, uri, guid, implicit=False): request = FakeRequest(root, "DELETE", uri) + request.authnUser = request.authzUser = davxml.Principal( + davxml.HRef.fromString("/principals/__uids__/%s/" % (guid,)) + ) # TODO: this seems hacky, even for a stub request: request._rememberResource(resource, uri) deleter = DeleteResource(request, resource, uri, - collection, "infinity", allowImplicitSchedule=False) + collection, "infinity", allowImplicitSchedule=implicit) return deleter.run() + + +@inlineCallbacks +def purgeGUID(guid, directory, root): + + # Does the record exist? + record = directory.recordWithGUID(guid) + if record is None: + # The user has already been removed from the directory service. We + # need to fashion a temporary, fake record + # FIXME: implement the fake record + pass + + principalCollection = directory.principalCollection + principal = principalCollection.principalForRecord(record) + calendarHome = principal.calendarHome() + + # Anything in the past should be deleted without implicit scheduling + now = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + filter = caldavxml.Filter( + caldavxml.ComponentFilter( + caldavxml.ComponentFilter( + TimeRange(start=now,), + name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), + ), + name="VCALENDAR", + ) + ) + + count = 0 + + for collName in calendarHome.listChildren(): + collection = calendarHome.getChild(collName) + if collection.isCalendarCollection(): + + # To compute past and ongoing events... + + # ...first start with all events... + allEvents = set(collection.listChildren()) + + ongoingEvents = set() + + # ...and find those that appear *after* the given cutoff + for name, uid, type in collection.index().indexedSearch(filter): + ongoingEvents.add(name) + + for name in allEvents: + resource = collection.getChild(name) + uri = "/calendars/__uids__/%s/%s/%s" % ( + record.uid, + collName, + name + ) + + (yield deleteResource(root, collection, resource, + uri, guid, implicit=(name in ongoingEvents))) + count += 1 + + returnValue(count) diff --git a/calendarserver/tools/test/deprovision/augments.xml b/calendarserver/tools/test/deprovision/augments.xml new file mode 100644 index 000000000..8121eb094 --- /dev/null +++ b/calendarserver/tools/test/deprovision/augments.xml @@ -0,0 +1,32 @@ + + + + + + + + + E9E78C86-4829-4520-A35D-70DDADAB2092 + true + true + + + 291C2C29-B663-4342-8EA1-A055E6A04D65 + true + true + + diff --git a/calendarserver/tools/test/deprovision/caldavd.plist b/calendarserver/tools/test/deprovision/caldavd.plist new file mode 100644 index 000000000..3971d8f65 --- /dev/null +++ b/calendarserver/tools/test/deprovision/caldavd.plist @@ -0,0 +1,767 @@ + + + + + + + + + + + + ServerHostName + + + + HTTPPort + 8008 + + + + SSLPort + 8443 + + + RedirectHTTPToHTTPS + + + + + + BindAddresses + + + + + BindHTTPPorts + + + + + BindSSLPorts + + + + + + + + ServerRoot + %(ServerRoot)s + + + DataRoot + Data + + + DocumentRoot + Documents + + + ConfigRoot + /etc/caldavd + + + LogRoot + /var/log/caldavd + + + RunRoot + /var/run + + + Aliases + + + + + + UserQuota + 104857600 + + + MaximumAttachmentSize + 1048576 + + + + MaxAttendeesPerInstance + 100 + + + + MaxInstancesForRRULE + 400 + + + + + + DirectoryService + + type + twistedcaldav.directory.xmlfile.XMLDirectoryService + + params + + xmlFile + accounts.xml + recordTypes + + users + groups + + + + + + ResourceService + + Enabled + + type + twistedcaldav.directory.xmlfile.XMLDirectoryService + + params + + xmlFile + resources.xml + recordTypes + + resources + locations + + cacheTimeout + 30 + + + + + + + + + + AugmentService + + type + twistedcaldav.directory.augment.AugmentXMLDB + + params + + xmlFiles + + augments.xml + + + + + + + + + + + + ProxyDBService + + type + twistedcaldav.directory.calendaruserproxy.ProxySqliteDB + + params + + dbpath + proxies.sqlite + + + + + + + ProxyLoadFromFile + conf/auth/proxies-test.xml + + + + + AdminPrincipals + + /principals/__uids__/admin/ + + + + ReadPrincipals + + + + + + SudoersFile + conf/sudoers.plist + + + EnableProxyPrincipals + + + + + + + EnableAnonymousReadRoot + + + + EnableAnonymousReadNav + + + + EnablePrincipalListings + + + + EnableMonolithicCalendars + + + + + + Authentication + + + + Basic + + Enabled + + + + + Digest + + Enabled + + Algorithm + md5 + Qop + + + + + Kerberos + + Enabled + + ServicePrincipal + + + + + Wiki + + Enabled + + Cookie + sessionID + URL + http://127.0.0.1/RPC2 + UserMethod + userForSession + WikiMethod + accessLevelForUserWikiCalendar + + + + + + + + + AccessLogFile + logs/access.log + RotateAccessLog + + + + ErrorLogFile + logs/error.log + + + DefaultLogLevel + warn + + + LogLevels + + + + + + GlobalStatsSocket + logs/caldavd-stats.sock + + + GlobalStatsLoggingPeriod + 60 + + + GlobalStatsLoggingFrequency + 12 + + + PIDFile + logs/caldavd.pid + + + + + + AccountingCategories + + iTIP + + HTTP + + + + AccountingPrincipals + + + + + + + + + SSLCertificate + twistedcaldav/test/data/server.pem + + + SSLAuthorityChain + + + + SSLPrivateKey + twistedcaldav/test/data/server.pem + + + + + UserName + + + GroupName + + + ProcessType + Combined + + MultiProcess + + ProcessCount + 2 + + + + + + Notifications + + + CoalesceSeconds + 3 + + InternalNotificationHost + localhost + + InternalNotificationPort + 62309 + + Services + + SimpleLineNotifier + + + Service + twistedcaldav.notify.SimpleLineNotifierService + Enabled + + Port + 62308 + + + XMPPNotifier + + + Service + twistedcaldav.notify.XMPPNotifierService + Enabled + + + + Host + xmpp.host.name + Port + 5222 + + + JID + jid@xmpp.host.name/resource + Password + password_goes_here + + + ServiceAddress + pubsub.xmpp.host.name + + NodeConfiguration + + pubsub#deliver_payloads + 1 + pubsub#persist_items + 1 + + + + KeepAliveSeconds + 120 + + + HeartbeatMinutes + 30 + + + AllowedJIDs + + + + + + + + + + + Scheduling + + + + CalDAV + + EmailDomain + + HTTPDomain + + AddressPatterns + + + OldDraftCompatibility + + ScheduleTagCompatibility + + EnablePrivateComments + + + + + iSchedule + + Enabled + + AddressPatterns + + + Servers + conf/servertoserver-test.xml + + + + iMIP + + Enabled + + MailGatewayServer + localhost + MailGatewayPort + 62310 + Sending + + Server + + Port + 587 + UseSSL + + Username + + Password + + Address + + + Receiving + + Server + + Port + 995 + Type + + UseSSL + + Username + + Password + + PollingSeconds + 30 + + AddressPatterns + + mailto:.* + + + + + Options + + AllowGroupAsOrganizer + + AllowLocationAsOrganizer + + AllowResourceAsOrganizer + + + + + + + + + FreeBusyURL + + Enabled + + TimePeriod + 14 + AnonymousAccess + + + + + + + + EnableDropBox + + + + EnablePrivateEvents + + + + EnableTimezoneService + + + + + + + EnableSACLs + + + + EnableWebAdmin + + + + ResponseCompression + + + + HTTPRetryAfter + 180 + + + ControlSocket + logs/caldavd.sock + + + Memcached + + MaxClients + 5 + memcached + memcached + Options + + + + Pools + + Default + + ClientEnabled + + ServerEnabled + + + + + + + + Twisted + + twistd + ../Twisted/bin/twistd + + + + Localization + + LocalesDirectory + locales + Language + English + + + + + diff --git a/calendarserver/tools/test/deprovision/resources-locations.xml b/calendarserver/tools/test/deprovision/resources-locations.xml new file mode 100644 index 000000000..bd146e7cc --- /dev/null +++ b/calendarserver/tools/test/deprovision/resources-locations.xml @@ -0,0 +1,34 @@ + + + + + + + + + location%02d + location%02d + location%02d + Room %02d + + + resource%02d + resource%02d + resource%02d + Resource %02d + + diff --git a/calendarserver/tools/test/deprovision/users-groups.xml b/calendarserver/tools/test/deprovision/users-groups.xml new file mode 100644 index 000000000..43a8aa043 --- /dev/null +++ b/calendarserver/tools/test/deprovision/users-groups.xml @@ -0,0 +1,38 @@ + + + + + + + + + deprovisioned + E9E78C86-4829-4520-A35D-70DDADAB2092 + test + Deprovisioned User + Deprovisioned + User + + + keeper + 291C2C29-B663-4342-8EA1-A055E6A04D65 + test + Keeper User + Keeper + User + + diff --git a/calendarserver/tools/test/test_purge.py b/calendarserver/tools/test/test_purge.py index 6cea0cfb0..cbfe62559 100644 --- a/calendarserver/tools/test/test_purge.py +++ b/calendarserver/tools/test/test_purge.py @@ -14,13 +14,18 @@ # limitations under the License. ## +from calendarserver.tap.util import getRootResource +from calendarserver.tools.purge import purgeOldEvents, purgeGUID +from datetime import datetime, timedelta +from twext.python.filepath import CachingFilePath as FilePath +from twext.python.plistlib import readPlistFromString +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, Deferred, returnValue +from twistedcaldav.config import config +from twistedcaldav.test.util import TestCase, CapturingProcessProtocol import os +import xml import zlib -from twistedcaldav.config import config -from twistedcaldav.test.util import TestCase -from twisted.internet.defer import inlineCallbacks -from calendarserver.tap.util import getRootResource -from calendarserver.tools.purge import purgeOldEvents resourceAttr = "WebDAV:{DAV:}resourcetype" collectionType = zlib.compress(""" @@ -41,7 +46,7 @@ def setUp(self): self.directory = self.rootResource.getDirectory() @inlineCallbacks - def test_purge(self): + def test_purgeOldEvents(self): before = { "calendars" : { "__uids__" : { @@ -329,3 +334,240 @@ def test_purge(self): END:VCALENDAR """.replace("\n", "\r\n") + + + +class DeprovisionTestCase(TestCase): + + def setUp(self): + super(DeprovisionTestCase, self).setUp() + + testRoot = os.path.join(os.path.dirname(__file__), "deprovision") + templateName = os.path.join(testRoot, "caldavd.plist") + templateFile = open(templateName) + template = templateFile.read() + templateFile.close() + + newConfig = template % { + "ServerRoot" : os.path.abspath(config.ServerRoot), + } + configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist")) + configFilePath.setContent(newConfig) + + self.configFileName = configFilePath.path + config.load(self.configFileName) + + os.makedirs(config.DataRoot) + os.makedirs(config.DocumentRoot) + + origUsersFile = FilePath(os.path.join(os.path.dirname(__file__), + "deprovision", "users-groups.xml")) + copyUsersFile = FilePath(os.path.join(config.DataRoot, "accounts.xml")) + origUsersFile.copyTo(copyUsersFile) + + origResourcesFile = FilePath(os.path.join(os.path.dirname(__file__), + "deprovision", "resources-locations.xml")) + copyResourcesFile = FilePath(os.path.join(config.DataRoot, "resources.xml")) + origResourcesFile.copyTo(copyResourcesFile) + + origAugmentFile = FilePath(os.path.join(os.path.dirname(__file__), + "deprovision", "augments.xml")) + copyAugmentFile = FilePath(os.path.join(config.DataRoot, "augments.xml")) + origAugmentFile.copyTo(copyAugmentFile) + + self.rootResource = getRootResource(config) + self.directory = self.rootResource.getDirectory() + + # Make sure trial puts the reactor in the right state, by letting it + # run one reactor iteration. (Ignore me, please.) + d = Deferred() + reactor.callLater(0, d.callback, True) + return d + + @inlineCallbacks + def runCommand(self, command, error=False): + """ + Run the given command by feeding it as standard input to + calendarserver_deprovision in a subprocess. + """ + sourceRoot = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + python = os.path.join(sourceRoot, "python") + script = os.path.join(sourceRoot, "bin", "calendarserver_purge_guid") + + args = [python, script, "-f", self.configFileName] + if error: + args.append("--error") + + cwd = sourceRoot + + deferred = Deferred() + reactor.spawnProcess(CapturingProcessProtocol(deferred, command), python, args, env=os.environ, path=cwd) + output = yield deferred + try: + plist = readPlistFromString(output) + except xml.parsers.expat.ExpatError, e: + print "Error (%s) parsing (%s)" % (e, output) + raise + + returnValue(plist) + + @inlineCallbacks + def test_purgeGUID(self): + # deprovision, add an event + + # Deprovisioned user is E9E78C86-4829-4520-A35D-70DDADAB2092 + # Keeper user is 291C2C29-B663-4342-8EA1-A055E6A04D65 + + before = { + "calendars" : { + "__uids__" : { + "E9" : { + "E7" : { + "E9E78C86-4829-4520-A35D-70DDADAB2092" : { + "calendar": { + "@xattrs" : + { + resourceAttr : collectionType, + }, + "noninvite.ics": { + "@contents" : NON_INVITE_ICS, + }, + "organizer.ics": { + "@contents" : ORGANIZER_ICS, + }, + "attendee.ics": { + "@contents" : ATTENDEE_ICS, + }, + }, + }, + }, + }, + "29" : { + "1C" : { + "291C2C29-B663-4342-8EA1-A055E6A04D65" : { + "calendar": { + "@xattrs" : + { + resourceAttr : collectionType, + }, + "organizer.ics": { + "@contents" : ORGANIZER_ICS, + }, + "attendee.ics": { + "@contents" : ATTENDEE_ICS, + }, + }, + }, + }, + }, + }, + }, + } + self.createHierarchy(before, config.DocumentRoot) + count = (yield purgeGUID("E9E78C86-4829-4520-A35D-70DDADAB2092", + self.directory, self.rootResource)) + + # print config.DocumentRoot + # import pdb; pdb.set_trace() + self.assertEquals(count, 3) + + after = { + "__uids__" : { + "E9" : { + "E7" : { + "E9E78C86-4829-4520-A35D-70DDADAB2092" : { + "calendar": { + ".db.sqlite": { + "@contents" : None, # ignore contents + }, + }, + }, + }, + }, + "29" : { + "1C" : { + "291C2C29-B663-4342-8EA1-A055E6A04D65" : { + "inbox": { + ".db.sqlite": { + "@contents" : None, # ignore contents + }, + "*.ics/UID:7ED97931-9A19-4596-9D4D-52B36D6AB803": { + "@contents" : ( + "METHOD:CANCEL", + ), + }, + "*.ics/UID:1974603C-B2C0-4623-92A0-2436DEAB07EF": { + "@contents" : ( + "METHOD:REPLY", + "ATTENDEE;CN=Deprovisioned User;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED:urn:uui\r\n d:E9E78C86-4829-4520-A35D-70DDADAB2092", + ), + }, + }, + "calendar": { + ".db.sqlite": { + "@contents" : None, # ignore contents + }, + "organizer.ics": { + "@contents" : ( + "STATUS:CANCELLED", + ), + }, + "attendee.ics": { + "@contents" : ( + "ATTENDEE;CN=Deprovisioned User;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;SCHEDUL\r\n E-STATUS=2.0:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092", + ), + }, + }, + }, + }, + }, + }, + } + self.assertTrue(self.verifyHierarchy( + os.path.join(config.DocumentRoot, "calendars"), + after) + ) + + +future = (datetime.utcnow() + timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ") +past = (datetime.utcnow() - timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ") + +NON_INVITE_ICS = """BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:151AFC76-6036-40EF-952B-97D1840760BF +SUMMARY:Non Invitation +DTSTART:%s +DURATION:PT1H +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") % (past,) + +ORGANIZER_ICS = """BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:7ED97931-9A19-4596-9D4D-52B36D6AB803 +SUMMARY:Organizer +DTSTART:%s +DURATION:PT1H +ORGANIZER:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092 +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092 +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65 +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") % (future,) + +ATTENDEE_ICS = """BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:1974603C-B2C0-4623-92A0-2436DEAB07EF +SUMMARY:Attendee +DTSTART:%s +DURATION:PT1H +ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65 +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:E9E78C86-4829-4520-A35D-70DDADAB2092 +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65 +END:VEVENT +END:VCALENDAR +""".replace("\n", "\r\n") % (future,) + diff --git a/twistedcaldav/test/util.py b/twistedcaldav/test/util.py index 785422213..0f6aaf663 100644 --- a/twistedcaldav/test/util.py +++ b/twistedcaldav/test/util.py @@ -119,16 +119,32 @@ def verifyChildren(parent, subStructure): actual.remove(childName) if childName.startswith("*"): + if "/" in childName: + childName, matching = childName.split("/") + else: + matching = False ext = childName.split(".")[1] found = False for actualFile in actual: if actualFile.endswith(ext): - actual.remove(actualFile) - found = True - break + matches = True + if matching: + matches = False + # We want to target only the wildcard file containing + # the matching string + actualPath = os.path.join(parent, actualFile) + with open(actualPath) as child: + contents = child.read() + if matching in contents: + matches = True + + if matches: + actual.remove(actualFile) + found = True + break if found: - continue - + # continue + childName = actualFile childPath = os.path.join(parent, childName) @@ -138,9 +154,18 @@ def verifyChildren(parent, subStructure): if childStructure.has_key("@contents"): # This is a file - if childStructure["@contents"] is None: + expectedContents = childStructure["@contents"] + if expectedContents is None: # We don't care about the contents pass + elif isinstance(expectedContents, tuple): + with open(childPath) as child: + contents = child.read() + for str in expectedContents: + if str not in contents: + print "Contents mismatch:", childPath + print "Expecting match:\n%s\n\nActual:\n%s\n" % (str, contents) + return False else: with open(childPath) as child: contents = child.read()