New test system based on doctest and unittest and many more.
This commit is contained in:
parent
53a945e78c
commit
546caa485d
21 changed files with 312 additions and 144 deletions
|
@ -13,6 +13,9 @@ mimeTypes = {'odt': 'application/vnd.oasis.opendocument.text',
|
|||
class UnmarshalledObject:
|
||||
'''Used for producing objects from a marshalled Python object (in some files
|
||||
like a CSV file or an XML file).'''
|
||||
def __init__(self, **fields):
|
||||
for k, v in fields.iteritems():
|
||||
setattr(self, k, v)
|
||||
def __repr__(self):
|
||||
res = u'<PythonObject '
|
||||
for attrName, attrValue in self.__dict__.iteritems():
|
||||
|
|
126
shared/test.py
126
shared/test.py
|
@ -17,12 +17,12 @@
|
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys, difflib, time, xml.sax
|
||||
from xml.sax.handler import ContentHandler
|
||||
import os, os.path, sys, time
|
||||
from optparse import OptionParser
|
||||
from appy.shared.utils import FolderDeleter, Traceback
|
||||
from appy.shared.errors import InternalError
|
||||
from appy.shared.rtf import RtfTablesParser
|
||||
from appy.shared.xml_parser import XmlComparator
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TesterError(Exception): pass
|
||||
|
@ -59,78 +59,6 @@ TEST_REPORT_SINGLETON_ERROR = 'You can only use the TestReport constructor ' \
|
|||
'TestReport instance via the TestReport.' \
|
||||
'instance static member.'
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class XmlHandler(ContentHandler):
|
||||
'''This handler is used for producing a readable XML (with carriage returns)
|
||||
and for removing some tags that always change (like dates) from a file
|
||||
that need to be compared to another file.'''
|
||||
def __init__(self, xmlTagsToIgnore, xmlAttrsToIgnore):
|
||||
ContentHandler.__init__(self)
|
||||
self.res = u'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
self.namespaces = {} # ~{s_namespaceUri:s_namespaceName}~
|
||||
self.indentLevel = -1
|
||||
self.tabWidth = 3
|
||||
self.tagsToIgnore = xmlTagsToIgnore
|
||||
self.attrsToIgnore = xmlAttrsToIgnore
|
||||
self.ignoring = False # Some content must be ignored, and not dumped
|
||||
# into the result.
|
||||
def isIgnorable(self, elem):
|
||||
'''Is p_elem an ignorable element ?'''
|
||||
res = False
|
||||
for nsUri, elemName in self.tagsToIgnore:
|
||||
elemFullName = ''
|
||||
try:
|
||||
nsName = self.ns(nsUri)
|
||||
elemFullName = '%s:%s' % (nsName, elemName)
|
||||
except KeyError:
|
||||
pass
|
||||
if elemFullName == elem:
|
||||
res = True
|
||||
break
|
||||
return res
|
||||
def setDocumentLocator(self, locator):
|
||||
self.locator = locator
|
||||
def endDocument(self):
|
||||
pass
|
||||
def dumpSpaces(self):
|
||||
self.res += '\n' + (' ' * self.indentLevel * self.tabWidth)
|
||||
def manageNamespaces(self, attrs):
|
||||
'''Manage namespaces definitions encountered in attrs'''
|
||||
for attrName, attrValue in attrs.items():
|
||||
if attrName.startswith('xmlns:'):
|
||||
self.namespaces[attrValue] = attrName[6:]
|
||||
def ns(self, nsUri):
|
||||
return self.namespaces[nsUri]
|
||||
def startElement(self, elem, attrs):
|
||||
self.manageNamespaces(attrs)
|
||||
# Do we enter into a ignorable element ?
|
||||
if self.isIgnorable(elem):
|
||||
self.ignoring = True
|
||||
else:
|
||||
if not self.ignoring:
|
||||
self.indentLevel += 1
|
||||
self.dumpSpaces()
|
||||
self.res += '<%s' % elem
|
||||
attrsNames = attrs.keys()
|
||||
attrsNames.sort()
|
||||
for attrToIgnore in self.attrsToIgnore:
|
||||
if attrToIgnore in attrsNames:
|
||||
attrsNames.remove(attrToIgnore)
|
||||
for attrName in attrsNames:
|
||||
self.res += ' %s="%s"' % (attrName, attrs[attrName])
|
||||
self.res += '>'
|
||||
def endElement(self, elem):
|
||||
if self.isIgnorable(elem):
|
||||
self.ignoring = False
|
||||
else:
|
||||
if not self.ignoring:
|
||||
self.dumpSpaces()
|
||||
self.indentLevel -= 1
|
||||
self.res += '</%s>' % elem
|
||||
def characters(self, content):
|
||||
if not self.ignoring:
|
||||
self.res += content.replace('\n', '')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class TestReport:
|
||||
instance = None
|
||||
|
@ -175,51 +103,11 @@ class Test:
|
|||
expectedFlavourSpecific = '%s.%s' % (expected, self.flavour)
|
||||
if os.path.exists(expectedFlavourSpecific):
|
||||
expected = expectedFlavourSpecific
|
||||
differ = difflib.Differ()
|
||||
if areXml:
|
||||
f = file(expected)
|
||||
contentA = f.read()
|
||||
f.close()
|
||||
# Actual result
|
||||
f = file(actual)
|
||||
contentB = f.read()
|
||||
f.close()
|
||||
xmlHandler = XmlHandler(xmlTagsToIgnore, xmlAttrsToIgnore)
|
||||
xml.sax.parseString(contentA, xmlHandler)
|
||||
contentA = xmlHandler.res.split('\n')
|
||||
xmlHandler = XmlHandler(xmlTagsToIgnore, xmlAttrsToIgnore)
|
||||
xml.sax.parseString(contentB, xmlHandler)
|
||||
contentB = xmlHandler.res.split('\n')
|
||||
else:
|
||||
f = file(expected)
|
||||
contentA = f.readlines()
|
||||
f.close()
|
||||
# Actual result
|
||||
f = file(actual)
|
||||
contentB = f.readlines()
|
||||
f.close()
|
||||
diffResult = list(differ.compare(contentA, contentB))
|
||||
atLeastOneDiff = False
|
||||
lastLinePrinted = False
|
||||
i = -1
|
||||
for line in diffResult:
|
||||
i += 1
|
||||
if line and (line[0] != ' '):
|
||||
if not atLeastOneDiff:
|
||||
self.report.say('Difference(s) detected between files ' \
|
||||
'%s and %s:' % (expected, actual),
|
||||
encoding='utf-8')
|
||||
atLeastOneDiff = True
|
||||
if not lastLinePrinted:
|
||||
self.report.say('...')
|
||||
if areXml:
|
||||
self.report.say(line, encoding=encoding)
|
||||
else:
|
||||
self.report.say(line[:-1], encoding=encoding)
|
||||
lastLinePrinted = True
|
||||
else:
|
||||
lastLinePrinted = False
|
||||
return atLeastOneDiff
|
||||
# Perform the comparison
|
||||
comparator = XmlComparator(expected, actual, areXml, xmlTagsToIgnore,
|
||||
xmlAttrsToIgnore)
|
||||
return not comparator.filesAreIdentical(
|
||||
report=self.report, encoding=encoding)
|
||||
def run(self):
|
||||
self.report.say('-' * 79)
|
||||
self.report.say('- Test %s.' % self.data['Name'])
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
import xml.sax
|
||||
import xml.sax, difflib
|
||||
from xml.sax.handler import ContentHandler, ErrorHandler
|
||||
from xml.sax.xmlreader import InputSource
|
||||
from StringIO import StringIO
|
||||
|
@ -188,6 +188,12 @@ class XmlUnmarshaller(XmlParser):
|
|||
# for example convert strings that have specific values (in this case,
|
||||
# knowing that the value is a 'string' is not sufficient).
|
||||
|
||||
def convertAttrs(self, attrs):
|
||||
'''Converts XML attrs to a dict.'''
|
||||
res = {}
|
||||
for k, v in attrs.items(): res[str(k)] = v
|
||||
return res
|
||||
|
||||
def startDocument(self):
|
||||
self.res = None # The resulting web of Python objects
|
||||
# (UnmarshalledObject instances).
|
||||
|
@ -210,7 +216,8 @@ class XmlUnmarshaller(XmlParser):
|
|||
elemType = self.tagTypes[elem]
|
||||
if elemType in self.containerTags:
|
||||
# I must create a new container object.
|
||||
if elemType == 'object': newObject = UnmarshalledObject()
|
||||
if elemType == 'object':
|
||||
newObject = UnmarshalledObject(**self.convertAttrs(attrs))
|
||||
elif elemType == 'tuple': newObject = [] # Tuples become lists
|
||||
elif elemType == 'list': newObject = []
|
||||
elif elemType == 'file':
|
||||
|
@ -219,7 +226,7 @@ class XmlUnmarshaller(XmlParser):
|
|||
newObject.name = attrs['name']
|
||||
if attrs.has_key('mimeType'):
|
||||
newObject.mimeType = attrs['mimeType']
|
||||
else: newObject = UnmarshalledObject()
|
||||
else: newObject = UnmarshalledObject(**self.convertAttrs(attrs))
|
||||
# Store the value on the last container, or on the root object.
|
||||
self.storeValue(elem, newObject)
|
||||
# Push the new object on the container stack
|
||||
|
@ -424,10 +431,12 @@ class XmlMarshaller:
|
|||
(Zope/Plone), specify 'archetype' for p_objectType.'''
|
||||
res = StringIO()
|
||||
# Dump the XML prologue and root element
|
||||
if objectType == 'archetype': objectId = instance.UID() # ID in DB
|
||||
else: objectId = str(id(instance)) # ID in RAM
|
||||
res.write(self.xmlPrologue)
|
||||
res.write('<'); res.write(self.rootElementName)
|
||||
res.write(' type="object">')
|
||||
# Dump the value of the fields that must be dumped
|
||||
res.write(' type="object" id="'); res.write(objectId); res.write('">')
|
||||
# Dump the object ID and the value of the fields that must be dumped
|
||||
if objectType == 'popo':
|
||||
for fieldName, fieldValue in instance.__dict__.iteritems():
|
||||
mustDump = False
|
||||
|
@ -479,4 +488,149 @@ class XmlMarshaller:
|
|||
result. p_res is the StringIO buffer where the result of the
|
||||
marshalling process is currently dumped; p_instance is the instance
|
||||
currently marshalled.'''
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class XmlHandler(ContentHandler):
|
||||
'''This handler is used for producing, in self.res, a readable XML
|
||||
(with carriage returns) and for removing some tags that always change
|
||||
(like dates) from a file that need to be compared to another file.'''
|
||||
def __init__(self, xmlTagsToIgnore, xmlAttrsToIgnore):
|
||||
ContentHandler.__init__(self)
|
||||
self.res = u'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
self.namespaces = {} # ~{s_namespaceUri:s_namespaceName}~
|
||||
self.indentLevel = -1
|
||||
self.tabWidth = 3
|
||||
self.tagsToIgnore = xmlTagsToIgnore
|
||||
self.attrsToIgnore = xmlAttrsToIgnore
|
||||
self.ignoring = False # Some content must be ignored, and not dumped
|
||||
# into the result.
|
||||
def isIgnorable(self, elem):
|
||||
'''Is p_elem an ignorable element ?'''
|
||||
res = False
|
||||
for tagName in self.tagsToIgnore:
|
||||
if isinstance(tagName, list) or isinstance(tagName, tuple):
|
||||
# We have a namespace
|
||||
nsUri, elemName = tagName
|
||||
try:
|
||||
nsName = self.ns(nsUri)
|
||||
elemFullName = '%s:%s' % (nsName, elemName)
|
||||
except KeyError:
|
||||
elemFullName = ''
|
||||
else:
|
||||
# No namespace
|
||||
elemFullName = tagName
|
||||
if elemFullName == elem:
|
||||
res = True
|
||||
break
|
||||
return res
|
||||
def setDocumentLocator(self, locator):
|
||||
self.locator = locator
|
||||
def endDocument(self):
|
||||
pass
|
||||
def dumpSpaces(self):
|
||||
self.res += '\n' + (' ' * self.indentLevel * self.tabWidth)
|
||||
def manageNamespaces(self, attrs):
|
||||
'''Manage namespaces definitions encountered in attrs'''
|
||||
for attrName, attrValue in attrs.items():
|
||||
if attrName.startswith('xmlns:'):
|
||||
self.namespaces[attrValue] = attrName[6:]
|
||||
def ns(self, nsUri):
|
||||
return self.namespaces[nsUri]
|
||||
def startElement(self, elem, attrs):
|
||||
self.manageNamespaces(attrs)
|
||||
# Do we enter into a ignorable element ?
|
||||
if self.isIgnorable(elem):
|
||||
self.ignoring = True
|
||||
else:
|
||||
if not self.ignoring:
|
||||
self.indentLevel += 1
|
||||
self.dumpSpaces()
|
||||
self.res += '<%s' % elem
|
||||
attrsNames = attrs.keys()
|
||||
attrsNames.sort()
|
||||
for attrToIgnore in self.attrsToIgnore:
|
||||
if attrToIgnore in attrsNames:
|
||||
attrsNames.remove(attrToIgnore)
|
||||
for attrName in attrsNames:
|
||||
self.res += ' %s="%s"' % (attrName, attrs[attrName])
|
||||
self.res += '>'
|
||||
def endElement(self, elem):
|
||||
if self.isIgnorable(elem):
|
||||
self.ignoring = False
|
||||
else:
|
||||
if not self.ignoring:
|
||||
self.dumpSpaces()
|
||||
self.indentLevel -= 1
|
||||
self.res += '</%s>' % elem
|
||||
def characters(self, content):
|
||||
if not self.ignoring:
|
||||
self.res += content.replace('\n', '')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class XmlComparator:
|
||||
'''Compares 2 XML files and produces a diff.'''
|
||||
def __init__(self, fileNameA, fileNameB, areXml=True, xmlTagsToIgnore=(),
|
||||
xmlAttrsToIgnore=()):
|
||||
self.fileNameA = fileNameA
|
||||
self.fileNameB = fileNameB
|
||||
self.areXml = areXml # Can also diff non-XML files.
|
||||
self.xmlTagsToIgnore = xmlTagsToIgnore
|
||||
self.xmlAttrsToIgnore = xmlAttrsToIgnore
|
||||
|
||||
def filesAreIdentical(self, report=None, encoding=None):
|
||||
'''Compares the 2 files and returns True if they are identical (if we
|
||||
ignore xmlTagsToIgnore and xmlAttrsToIgnore).
|
||||
If p_report is specified, it must be an instance of
|
||||
appy.shared.test.TestReport; the diffs will be dumped in it.'''
|
||||
# Perform the comparison
|
||||
differ = difflib.Differ()
|
||||
if self.areXml:
|
||||
f = file(self.fileNameA)
|
||||
contentA = f.read()
|
||||
f.close()
|
||||
f = file(self.fileNameB)
|
||||
contentB = f.read()
|
||||
f.close()
|
||||
xmlHandler = XmlHandler(self.xmlTagsToIgnore, self.xmlAttrsToIgnore)
|
||||
xml.sax.parseString(contentA, xmlHandler)
|
||||
contentA = xmlHandler.res.split('\n')
|
||||
xmlHandler = XmlHandler(self.xmlTagsToIgnore, self.xmlAttrsToIgnore)
|
||||
xml.sax.parseString(contentB, xmlHandler)
|
||||
contentB = xmlHandler.res.split('\n')
|
||||
else:
|
||||
f = file(self.fileNameA)
|
||||
contentA = f.readlines()
|
||||
f.close()
|
||||
f = file(self.fileNameB)
|
||||
contentB = f.readlines()
|
||||
f.close()
|
||||
diffResult = list(differ.compare(contentA, contentB))
|
||||
# Analyse, format and report the result.
|
||||
atLeastOneDiff = False
|
||||
lastLinePrinted = False
|
||||
i = -1
|
||||
for line in diffResult:
|
||||
i += 1
|
||||
if line and (line[0] != ' '):
|
||||
if not atLeastOneDiff:
|
||||
if report:
|
||||
report.say('Difference(s) detected between files '\
|
||||
'%s and %s:' % (self.fileNameA, self.fileNameB),
|
||||
encoding='utf-8')
|
||||
else:
|
||||
print 'Differences:'
|
||||
atLeastOneDiff = True
|
||||
if not lastLinePrinted:
|
||||
if report: report.say('...')
|
||||
else: print '...'
|
||||
if self.areXml:
|
||||
if report: report.say(line, encoding=encoding)
|
||||
else: print line
|
||||
else:
|
||||
if report: report.say(line[:-1], encoding=encoding)
|
||||
else: print line[:-1]
|
||||
lastLinePrinted = True
|
||||
else:
|
||||
lastLinePrinted = False
|
||||
return not atLeastOneDiff
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue