New test system based on doctest and unittest and many more.

This commit is contained in:
Gaetan Delannay 2009-11-11 20:22:13 +01:00
parent 53a945e78c
commit 546caa485d
21 changed files with 312 additions and 144 deletions

View file

@ -127,7 +127,7 @@ class Integer(Type):
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False): master=None, masterValue=None, focus=False):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False, editDefault, show, page, group, move, indexed, searchable,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus) height, master, masterValue, focus)
self.pythonType = long self.pythonType = long
@ -549,4 +549,9 @@ class Config:
# If you don't need the portlet that appy.gen has generated for your # If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False. # application, set the following parameter to False.
self.showPortlet = True self.showPortlet = True
# Default number of flavours. It will be used for generating i18n labels
# for classes in every flavour. Indeed, every flavour can name its
# concepts differently. For example, class Thing in flavour 2 may have
# i18n label "MyProject_Thing_2".
self.numberOfFlavours = 2
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -150,6 +150,7 @@ class Generator:
self.workflows = [] self.workflows = []
self.initialize() self.initialize()
self.config = Config.getDefault() self.config = Config.getDefault()
self.modulesWithTests = set()
def determineAppyType(self, klass): def determineAppyType(self, klass):
'''Is p_klass an Appy class ? An Appy workflow? None of this ? '''Is p_klass an Appy class ? An Appy workflow? None of this ?
@ -171,6 +172,12 @@ class Generator:
break break
return res return res
def containsTests(self, moduleOrClass):
'''Does p_moduleOrClass contain doctests?'''
if moduleOrClass.__doc__ and (moduleOrClass.__doc__.find('>>>') != -1):
return True
return False
IMPORT_ERROR = 'Warning: error while importing module %s (%s)' IMPORT_ERROR = 'Warning: error while importing module %s (%s)'
SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)' SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)'
def walkModule(self, moduleName): def walkModule(self, moduleName):
@ -190,6 +197,8 @@ class Generator:
except SyntaxError, se: except SyntaxError, se:
print self.SYNTAX_ERROR % (moduleName, str(se)) print self.SYNTAX_ERROR % (moduleName, str(se))
return return
if self.containsTests(moduleObj):
self.modulesWithTests.add(moduleObj.__name__)
classType = type(Generator) classType = type(Generator)
# Find all classes in this module # Find all classes in this module
for moduleElemName in moduleObj.__dict__.keys(): for moduleElemName in moduleObj.__dict__.keys():
@ -215,10 +224,14 @@ class Generator:
descrClass = self.classDescriptor descrClass = self.classDescriptor
self.classes.append( self.classes.append(
descrClass(moduleElem, attrs, self)) descrClass(moduleElem, attrs, self))
if self.containsTests(moduleElem):
self.modulesWithTests.add(moduleObj.__name__)
elif appyType == 'workflow': elif appyType == 'workflow':
descrClass = self.workflowDescriptor descrClass = self.workflowDescriptor
self.workflows.append( self.workflows.append(
descrClass(moduleElem, attrs, self)) descrClass(moduleElem, attrs, self))
if self.containsTests(moduleElem):
self.modulesWithTests.add(moduleObj.__name__)
elif isinstance(moduleElem, Config): elif isinstance(moduleElem, Config):
self.config = moduleElem self.config = moduleElem

View file

@ -132,6 +132,7 @@ class Generator(AbstractGenerator):
self.generateInstall() self.generateInstall()
self.generateWorkflows() self.generateWorkflows()
self.generateWrappers() self.generateWrappers()
self.generateTests()
if self.config.frontPage == True: if self.config.frontPage == True:
self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT)) self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT))
self.copyFile('frontPage.pt', self.repls, self.copyFile('frontPage.pt', self.repls,
@ -155,8 +156,8 @@ class Generator(AbstractGenerator):
f = open(os.path.join(self.outputFolder, 'version.txt'), 'w') f = open(os.path.join(self.outputFolder, 'version.txt'), 'w')
f.write(self.version) f.write(self.version)
f.close() f.close()
# Make Extensions a Python package # Make Extensions and tests Python packages
for moduleFolder in ('Extensions',): for moduleFolder in ('Extensions', 'tests'):
initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder) initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder)
if not os.path.isfile(initFile): if not os.path.isfile(initFile):
f = open(initFile, 'w') f = open(initFile, 'w')
@ -539,6 +540,14 @@ class Generator(AbstractGenerator):
repls['podTemplateBody'] = PodTemplate._appy_getBody() repls['podTemplateBody'] = PodTemplate._appy_getBody()
self.copyFile('appyWrappers.py', repls, destFolder='Extensions') self.copyFile('appyWrappers.py', repls, destFolder='Extensions')
def generateTests(self):
'''Generates the file needed for executing tests.'''
repls = self.repls.copy()
modules = self.modulesWithTests
repls['imports'] = '\n'.join(['import %s' % m for m in modules])
repls['modulesWithTests'] = ','.join(modules)
self.copyFile('testAll.py', repls, destFolder='tests')
def generateTool(self): def generateTool(self):
'''Generates the Plone tool that corresponds to this application.''' '''Generates the Plone tool that corresponds to this application.'''
# Generate the tool class in itself and related i18n messages # Generate the tool class in itself and related i18n messages
@ -672,7 +681,7 @@ class Generator(AbstractGenerator):
poMsgPl.produceNiceDefault() poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl) self.labels.append(poMsgPl)
# Create i18n labels for flavoured variants # Create i18n labels for flavoured variants
for i in range(2,10): for i in range(2, self.config.numberOfFlavours+1):
poMsg = PoMessage('%s_%d' % (classDescr.name, i), '', poMsg = PoMessage('%s_%d' % (classDescr.name, i), '',
classDescr.klass.__name__) classDescr.klass.__name__)
poMsg.produceNiceDefault() poMsg.produceNiceDefault()

View file

@ -104,6 +104,7 @@ class PloneInstaller:
if not hasattr(site.portal_types, self.appyFolderType): if not hasattr(site.portal_types, self.appyFolderType):
self.registerAppyFolderType() self.registerAppyFolderType()
# Create the folder # Create the folder
if not hasattr(site.aq_base, self.productName): if not hasattr(site.aq_base, self.productName):
# Temporarily allow me to create Appy large plone folders # Temporarily allow me to create Appy large plone folders
getattr(site.portal_types, self.appyFolderType).global_allow = 1 getattr(site.portal_types, self.appyFolderType).global_allow = 1
@ -114,6 +115,7 @@ class PloneInstaller:
title=self.productName) title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0 getattr(site.portal_types, self.appyFolderType).global_allow = 0
appFolder = getattr(site, self.productName) appFolder = getattr(site, self.productName)
# All roles defined as creators should be able to create the # All roles defined as creators should be able to create the
# corresponding root content types in this folder. # corresponding root content types in this folder.
i = -1 i = -1

View file

@ -0,0 +1,27 @@
# ------------------------------------------------------------------------------
class TestMixin:
'''This class is mixed in with any PloneTestCase.'''
def createUser(self, userId, roles):
'''Creates a user p_name p_with some p_roles.'''
pms = self.portal.portal_membership
pms.addMember(userId, 'password', [], [])
self.setRoles(roles, name=userId)
def changeUser(self, userId):
'''Logs out currently logged user and logs in p_loginName.'''
self.logout()
self.login(userId)
# Functions executed before and after every test -------------------------------
def beforeTest(test):
g = test.globs
g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy()
g['appFolder'] = g['tool'].o.getProductConfig().diskFolder
moduleOrClassName = g['test'].name # Not used yet.
# Initialize the test
test.createUser('admin', ('Member','Manager'))
test.login('admin')
g['t'] = g['test']
def afterTest(test): pass
# ------------------------------------------------------------------------------

View file

@ -1024,7 +1024,8 @@ class AbstractMixin:
self._appy_manageRefsFromRequest() self._appy_manageRefsFromRequest()
# If the creation was initiated by another object, update the # If the creation was initiated by another object, update the
# reference. # reference.
if created: if created and hasattr(self.REQUEST, 'SESSION'):
# When used by the test system, no SESSION object is created.
session = self.REQUEST.SESSION session = self.REQUEST.SESSION
initiatorUid = session.get('initiator', None) initiatorUid = session.get('initiator', None)
initiator = None initiator = None

View file

@ -200,7 +200,7 @@
<metal:group define-macro="showGroup"> <metal:group define-macro="showGroup">
<fieldset class="appyGroup"> <fieldset class="appyGroup">
<legend><i tal:define="groupDescription python:contextObj.translate('%s_group_%s' % (contextObj.meta_type, widgetDescr['name']))" <legend><i tal:define="groupDescription python:contextObj.translate('%s_group_%s' % (contextObj.meta_type, widgetDescr['name']))"
tal:content="groupDescription"></i></legend> tal:content="structure groupDescription"></i></legend>
<table tal:define="global fieldNb python:-1" width="100%"> <table tal:define="global fieldNb python:-1" width="100%">
<tr valign="top" tal:repeat="rowNb python:range(widgetDescr['rows'])"> <tr valign="top" tal:repeat="rowNb python:range(widgetDescr['rows'])">
<td tal:repeat="colNb python:range(widgetDescr['cols'])" <td tal:repeat="colNb python:range(widgetDescr['cols'])"
@ -754,8 +754,8 @@
var state = readCookie(groupId); var state = readCookie(groupId);
if ((state != 'collapsed') && (state != 'expanded')) { if ((state != 'collapsed') && (state != 'expanded')) {
// No cookie yet, create it. // No cookie yet, create it.
createCookie(groupId, 'expanded'); createCookie(groupId, 'collapsed');
state = 'expanded'; state = 'collapsed';
} }
var group = document.getElementById(groupId); var group = document.getElementById(groupId);
var displayValue = 'none'; var displayValue = 'none';
@ -829,7 +829,7 @@
<tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)"> <tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)">
<tal:group condition="searchOrGroup/isGroup"> <tal:group condition="searchOrGroup/isGroup">
<tal:expanded define="group searchOrGroup; <tal:expanded define="group searchOrGroup;
expanded python: tool.getCookieValue(group['labelId']) == 'expanded'"> expanded python: tool.getCookieValue(group['labelId'], default='collapsed') == 'expanded'">
<tal:comment replace="nothing">Group name</tal:comment> <tal:comment replace="nothing">Group name</tal:comment>
<dt class="portletAppyItem portletGroup"> <dt class="portletAppyItem portletGroup">
<img align="left" style="cursor:pointer" <img align="left" style="cursor:pointer"
@ -839,15 +839,15 @@
<span tal:replace="group/label"/> <span tal:replace="group/label"/>
</dt> </dt>
<tal:comment replace="nothing">Group searches</tal:comment> <tal:comment replace="nothing">Group searches</tal:comment>
<div tal:attributes="id group/labelId; <span tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')"> style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletAppyItem portletSearch" tal:repeat="search group/searches"> <dt class="portletAppyItem portletSearch portletGroupItem" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']); <a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title search/descr; title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');" class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a> tal:content="structure search/label"></a>
</dt> </dt>
</div> </span>
</tal:expanded> </tal:expanded>
</tal:group> </tal:group>
<dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup" <dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup"
@ -855,8 +855,7 @@
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']); <a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title search/descr; title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', ''); class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
id search/group"
tal:content="structure search/label"></a> tal:content="structure search/label"></a>
</dt> </dt>
</tal:searchOrGroup> </tal:searchOrGroup>

View file

@ -1,5 +1,6 @@
<!codeHeader!> <!codeHeader!>
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config import Products.<!applicationName!>.config
from Extensions.appyWrappers import <!genClassName!>_Wrapper from Extensions.appyWrappers import <!genClassName!>_Wrapper

View file

@ -1,5 +1,6 @@
<!codeHeader!> <!codeHeader!>
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin

View file

@ -1,6 +1,12 @@
/* Appy-specific IE-fixes */ /* Appy-specific IE-fixes */
.portletSearch { .portletSearch {
font-size: 85%; font-size: 85%;
border-left: 1px solid #8cacbb;
border-right: 1px solid #8cacbb;
}
.portletGroup {
font-size: 85%;
padding-left: 0.7em;
} }
/* Stylesheet with Internet Explorer-specific workarounds. */ /* Stylesheet with Internet Explorer-specific workarounds. */

View file

@ -1,5 +1,6 @@
<!codeHeader!> <!codeHeader!>
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
import Products.<!applicationName!>.config import Products.<!applicationName!>.config
from appy.gen.plone25.mixins.PodTemplateMixin import PodTemplateMixin from appy.gen.plone25.mixins.PodTemplateMixin import PodTemplateMixin

View file

@ -229,7 +229,7 @@ fieldset {
} }
.portletSearch { .portletSearch {
padding: 0 0 0 0.6em; padding: 0 0 0 0.6em;
font-style: italic; font-style: normal;
font-size: 95%; font-size: 95%;
} }
.portletGroup { .portletGroup {
@ -237,6 +237,10 @@ fieldset {
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
.portletGroupItem {
padding-left: 0.8em;
font-style: italic;
}
.portletCurrent { .portletCurrent {
font-weight: bold; font-weight: bold;
} }

View file

@ -1,5 +1,6 @@
<!codeHeader!> <!codeHeader!>
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from DateTime import DateTime
from Products.Archetypes.atapi import * from Products.Archetypes.atapi import *
from Products.CMFCore.utils import UniqueObject from Products.CMFCore.utils import UniqueObject
import Products.<!applicationName!>.config import Products.<!applicationName!>.config

View file

@ -1,5 +1,5 @@
<!codeHeader!> <!codeHeader!>
import sys import os, os.path, sys
try: # New CMF try: # New CMF
from Products.CMFCore.permissions import setDefaultRoles from Products.CMFCore.permissions import setDefaultRoles
except ImportError: # Old CMF except ImportError: # Old CMF
@ -7,7 +7,7 @@ except ImportError: # Old CMF
import Extensions.appyWrappers import Extensions.appyWrappers
<!imports!> <!imports!>
# The following imports are here for allowing mixin classes to access those # The following imports are here for allowing mixin classes to access those
# elements without being statically dependent on Plone/Zope packages. Indeed, # elements without being statically dependent on Plone/Zope packages. Indeed,
# every Archetype instance has a method "getProductConfig" that returns this # every Archetype instance has a method "getProductConfig" that returns this
@ -23,6 +23,7 @@ logger = logging.getLogger('<!applicationName!>')
# Some global variables -------------------------------------------------------- # Some global variables --------------------------------------------------------
PROJECTNAME = '<!applicationName!>' PROJECTNAME = '<!applicationName!>'
diskFolder = os.path.dirname(<!applicationName!>.__file__)
defaultAddRoles = [<!defaultAddRoles!>] defaultAddRoles = [<!defaultAddRoles!>]
DEFAULT_ADD_CONTENT_PERMISSION = "Add portal content" DEFAULT_ADD_CONTENT_PERMISSION = "Add portal content"
ADD_CONTENT_PERMISSIONS = { ADD_CONTENT_PERMISSIONS = {

View file

@ -0,0 +1,25 @@
<!codeHeader!>
from unittest import TestSuite
from Testing import ZopeTestCase
from Testing.ZopeTestCase import ZopeDocTestSuite
from Products.PloneTestCase import PloneTestCase
from appy.gen.plone25.mixins.TestMixin import TestMixin, beforeTest, afterTest
<!imports!>
# Initialize Zope & Plone test systems -----------------------------------------
ZopeTestCase.installProduct('<!applicationName!>')
PloneTestCase.setupPloneSite(products=['<!applicationName!>'])
class Test(PloneTestCase.PloneTestCase, TestMixin):
'''Base test class for <!applicationName!> test cases.'''
# Data needed for defining the tests -------------------------------------------
data = {'test_class': Test, 'setUp': beforeTest, 'tearDown': afterTest,
'globs': {'appName': '<!applicationName!>'}}
modulesWithTests = [<!modulesWithTests!>]
# ------------------------------------------------------------------------------
def test_suite():
return TestSuite([ZopeDocTestSuite(m, **data) for m in modulesWithTests])
# ------------------------------------------------------------------------------

View file

@ -7,6 +7,8 @@ def stringify(value):
for v in value: for v in value:
res += '%s,' % stringify(v) res += '%s,' % stringify(v)
res += ')' res += ')'
elif value.__class__.__name__ == 'DateTime':
res = 'DateTime("%s")' % value.strftime('%Y/%m/%d %H:%M')
else: else:
res = str(value) res = str(value)
if isinstance(value, basestring): if isinstance(value, basestring):

View file

@ -6,6 +6,7 @@ import time, os.path, mimetypes, unicodedata
from appy.gen import Search from appy.gen import Search
from appy.gen.utils import sequenceTypes from appy.gen.utils import sequenceTypes
from appy.shared.utils import getOsTempFolder from appy.shared.utils import getOsTempFolder
from appy.shared.xml_parser import XmlMarshaller
# Some error messages ---------------------------------------------------------- # Some error messages ----------------------------------------------------------
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \ WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
@ -68,6 +69,8 @@ class AbstractWrapper:
self._set_file_attribute(name, v) self._set_file_attribute(name, v)
else: else:
exec "self.o.set%s%s(v)" % (name[0].upper(), name[1:]) exec "self.o.set%s%s(v)" % (name[0].upper(), name[1:])
def __repr__(self):
return '<%s wrapper at %s>' % (self.klass.__name__, id(self))
def __cmp__(self, other): def __cmp__(self, other):
if other: return cmp(self.o, other.o) if other: return cmp(self.o, other.o)
else: return 1 else: return 1
@ -258,6 +261,26 @@ class AbstractWrapper:
method in those cases.''' method in those cases.'''
self.o.reindexObject() self.o.reindexObject()
def export(self, at='string'):
'''Creates an "exportable", XML version of this object. If p_at is
"string", this method returns the XML version. Else, (a) if not p_at,
the XML will be exported on disk, in the OS temp folder, with an
ugly name; (b) else, it will be exported at path p_at.'''
# Determine where to put the result
toDisk = (at != 'string')
if toDisk and not at:
at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
# Create the XML version of the object
xml = XmlMarshaller().marshall(self.o, objectType='archetype')
# Produce the desired result
if toDisk:
f = file(at, 'w')
f.write(xml)
f.close()
return at
else:
return xml
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class FileWrapper: class FileWrapper:
'''When you get, from an appy object, the value of a File attribute, you '''When you get, from an appy object, the value of a File attribute, you

View file

@ -17,7 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import sys, os, os.path, time, signal import sys, os, os.path, time, signal, unicodedata
from optparse import OptionParser from optparse import OptionParser
ODT_FILE_TYPES = {'doc': 'MS Word 97', # Could be 'MS Word 2003 XML' ODT_FILE_TYPES = {'doc': 'MS Word 97', # Could be 'MS Word 2003 XML'
@ -55,6 +55,8 @@ class Converter:
def __init__(self, docPath, resultType, port=DEFAULT_PORT): def __init__(self, docPath, resultType, port=DEFAULT_PORT):
self.port = port self.port = port
self.docUrl = self.getDocUrl(docPath) self.docUrl = self.getDocUrl(docPath)
self.docUrlStr = unicodedata.normalize('NFKD', self.docUrl).encode(
"ascii", "ignore")
self.resultFilter = self.getResultFilter(resultType) self.resultFilter = self.getResultFilter(resultType)
self.resultUrl = self.getResultUrl(resultType) self.resultUrl = self.getResultUrl(resultType)
self.ooContext = None self.ooContext = None
@ -74,7 +76,7 @@ class Converter:
ODT_FILE_TYPES.keys())) ODT_FILE_TYPES.keys()))
return res return res
def getResultUrl(self, resultType): def getResultUrl(self, resultType):
baseName = os.path.splitext(self.docUrl)[0] baseName = os.path.splitext(self.docUrlStr)[0]
if resultType != 'odt': if resultType != 'odt':
res = '%s.%s' % (baseName, resultType) res = '%s.%s' % (baseName, resultType)
else: else:
@ -164,7 +166,7 @@ class Converter:
except IndexOutOfBoundsException: except IndexOutOfBoundsException:
pass pass
except IllegalArgumentException, iae: except IllegalArgumentException, iae:
raise ConverterError(URL_NOT_FOUND % (self.docUrl, iae)) raise ConverterError(URL_NOT_FOUND % (self.docUrlStr, iae))
def convertDocument(self): def convertDocument(self):
if self.resultFilter != 'ODT': if self.resultFilter != 'ODT':
# I must really perform a conversion # I must really perform a conversion

View file

@ -13,6 +13,9 @@ mimeTypes = {'odt': 'application/vnd.oasis.opendocument.text',
class UnmarshalledObject: class UnmarshalledObject:
'''Used for producing objects from a marshalled Python object (in some files '''Used for producing objects from a marshalled Python object (in some files
like a CSV file or an XML file).''' 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): def __repr__(self):
res = u'<PythonObject ' res = u'<PythonObject '
for attrName, attrValue in self.__dict__.iteritems(): for attrName, attrValue in self.__dict__.iteritems():

View file

@ -17,12 +17,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys, difflib, time, xml.sax import os, os.path, sys, time
from xml.sax.handler import ContentHandler
from optparse import OptionParser from optparse import OptionParser
from appy.shared.utils import FolderDeleter, Traceback from appy.shared.utils import FolderDeleter, Traceback
from appy.shared.errors import InternalError from appy.shared.errors import InternalError
from appy.shared.rtf import RtfTablesParser from appy.shared.rtf import RtfTablesParser
from appy.shared.xml_parser import XmlComparator
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class TesterError(Exception): pass class TesterError(Exception): pass
@ -59,78 +59,6 @@ TEST_REPORT_SINGLETON_ERROR = 'You can only use the TestReport constructor ' \
'TestReport instance via the TestReport.' \ 'TestReport instance via the TestReport.' \
'instance static member.' '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: class TestReport:
instance = None instance = None
@ -175,51 +103,11 @@ class Test:
expectedFlavourSpecific = '%s.%s' % (expected, self.flavour) expectedFlavourSpecific = '%s.%s' % (expected, self.flavour)
if os.path.exists(expectedFlavourSpecific): if os.path.exists(expectedFlavourSpecific):
expected = expectedFlavourSpecific expected = expectedFlavourSpecific
differ = difflib.Differ() # Perform the comparison
if areXml: comparator = XmlComparator(expected, actual, areXml, xmlTagsToIgnore,
f = file(expected) xmlAttrsToIgnore)
contentA = f.read() return not comparator.filesAreIdentical(
f.close() report=self.report, encoding=encoding)
# 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
def run(self): def run(self):
self.report.say('-' * 79) self.report.say('-' * 79)
self.report.say('- Test %s.' % self.data['Name']) self.report.say('- Test %s.' % self.data['Name'])

View file

@ -17,7 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA. # 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.handler import ContentHandler, ErrorHandler
from xml.sax.xmlreader import InputSource from xml.sax.xmlreader import InputSource
from StringIO import StringIO from StringIO import StringIO
@ -188,6 +188,12 @@ class XmlUnmarshaller(XmlParser):
# for example convert strings that have specific values (in this case, # for example convert strings that have specific values (in this case,
# knowing that the value is a 'string' is not sufficient). # 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): def startDocument(self):
self.res = None # The resulting web of Python objects self.res = None # The resulting web of Python objects
# (UnmarshalledObject instances). # (UnmarshalledObject instances).
@ -210,7 +216,8 @@ class XmlUnmarshaller(XmlParser):
elemType = self.tagTypes[elem] elemType = self.tagTypes[elem]
if elemType in self.containerTags: if elemType in self.containerTags:
# I must create a new container object. # 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 == 'tuple': newObject = [] # Tuples become lists
elif elemType == 'list': newObject = [] elif elemType == 'list': newObject = []
elif elemType == 'file': elif elemType == 'file':
@ -219,7 +226,7 @@ class XmlUnmarshaller(XmlParser):
newObject.name = attrs['name'] newObject.name = attrs['name']
if attrs.has_key('mimeType'): if attrs.has_key('mimeType'):
newObject.mimeType = attrs['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. # Store the value on the last container, or on the root object.
self.storeValue(elem, newObject) self.storeValue(elem, newObject)
# Push the new object on the container stack # Push the new object on the container stack
@ -424,10 +431,12 @@ class XmlMarshaller:
(Zope/Plone), specify 'archetype' for p_objectType.''' (Zope/Plone), specify 'archetype' for p_objectType.'''
res = StringIO() res = StringIO()
# Dump the XML prologue and root element # 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(self.xmlPrologue)
res.write('<'); res.write(self.rootElementName) res.write('<'); res.write(self.rootElementName)
res.write(' type="object">') res.write(' type="object" id="'); res.write(objectId); res.write('">')
# Dump the value of the fields that must be dumped # Dump the object ID and the value of the fields that must be dumped
if objectType == 'popo': if objectType == 'popo':
for fieldName, fieldValue in instance.__dict__.iteritems(): for fieldName, fieldValue in instance.__dict__.iteritems():
mustDump = False mustDump = False
@ -479,4 +488,149 @@ class XmlMarshaller:
result. p_res is the StringIO buffer where the result of the result. p_res is the StringIO buffer where the result of the
marshalling process is currently dumped; p_instance is the instance marshalling process is currently dumped; p_instance is the instance
currently marshalled.''' 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------