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,
master=None, masterValue=None, focus=False):
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,
height, master, masterValue, focus)
self.pythonType = long
@ -549,4 +549,9 @@ class Config:
# If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False.
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.initialize()
self.config = Config.getDefault()
self.modulesWithTests = set()
def determineAppyType(self, klass):
'''Is p_klass an Appy class ? An Appy workflow? None of this ?
@ -171,6 +172,12 @@ class Generator:
break
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)'
SYNTAX_ERROR = 'Warning: error while parsing module %s (%s)'
def walkModule(self, moduleName):
@ -190,6 +197,8 @@ class Generator:
except SyntaxError, se:
print self.SYNTAX_ERROR % (moduleName, str(se))
return
if self.containsTests(moduleObj):
self.modulesWithTests.add(moduleObj.__name__)
classType = type(Generator)
# Find all classes in this module
for moduleElemName in moduleObj.__dict__.keys():
@ -215,10 +224,14 @@ class Generator:
descrClass = self.classDescriptor
self.classes.append(
descrClass(moduleElem, attrs, self))
if self.containsTests(moduleElem):
self.modulesWithTests.add(moduleObj.__name__)
elif appyType == 'workflow':
descrClass = self.workflowDescriptor
self.workflows.append(
descrClass(moduleElem, attrs, self))
if self.containsTests(moduleElem):
self.modulesWithTests.add(moduleObj.__name__)
elif isinstance(moduleElem, Config):
self.config = moduleElem

View file

@ -132,6 +132,7 @@ class Generator(AbstractGenerator):
self.generateInstall()
self.generateWorkflows()
self.generateWrappers()
self.generateTests()
if self.config.frontPage == True:
self.labels.append(msg('front_page_text', '', msg.FRONT_PAGE_TEXT))
self.copyFile('frontPage.pt', self.repls,
@ -155,8 +156,8 @@ class Generator(AbstractGenerator):
f = open(os.path.join(self.outputFolder, 'version.txt'), 'w')
f.write(self.version)
f.close()
# Make Extensions a Python package
for moduleFolder in ('Extensions',):
# Make Extensions and tests Python packages
for moduleFolder in ('Extensions', 'tests'):
initFile = '%s/%s/__init__.py' % (self.outputFolder, moduleFolder)
if not os.path.isfile(initFile):
f = open(initFile, 'w')
@ -539,6 +540,14 @@ class Generator(AbstractGenerator):
repls['podTemplateBody'] = PodTemplate._appy_getBody()
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):
'''Generates the Plone tool that corresponds to this application.'''
# Generate the tool class in itself and related i18n messages
@ -672,7 +681,7 @@ class Generator(AbstractGenerator):
poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl)
# 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), '',
classDescr.klass.__name__)
poMsg.produceNiceDefault()

View file

@ -104,6 +104,7 @@ class PloneInstaller:
if not hasattr(site.portal_types, self.appyFolderType):
self.registerAppyFolderType()
# Create the folder
if not hasattr(site.aq_base, self.productName):
# Temporarily allow me to create Appy large plone folders
getattr(site.portal_types, self.appyFolderType).global_allow = 1
@ -114,6 +115,7 @@ class PloneInstaller:
title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0
appFolder = getattr(site, self.productName)
# All roles defined as creators should be able to create the
# corresponding root content types in this folder.
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()
# If the creation was initiated by another object, update the
# 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
initiatorUid = session.get('initiator', None)
initiator = None

View file

@ -200,7 +200,7 @@
<metal:group define-macro="showGroup">
<fieldset class="appyGroup">
<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%">
<tr valign="top" tal:repeat="rowNb python:range(widgetDescr['rows'])">
<td tal:repeat="colNb python:range(widgetDescr['cols'])"
@ -754,8 +754,8 @@
var state = readCookie(groupId);
if ((state != 'collapsed') && (state != 'expanded')) {
// No cookie yet, create it.
createCookie(groupId, 'expanded');
state = 'expanded';
createCookie(groupId, 'collapsed');
state = 'collapsed';
}
var group = document.getElementById(groupId);
var displayValue = 'none';
@ -829,7 +829,7 @@
<tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)">
<tal:group condition="searchOrGroup/isGroup">
<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>
<dt class="portletAppyItem portletGroup">
<img align="left" style="cursor:pointer"
@ -839,15 +839,15 @@
<span tal:replace="group/label"/>
</dt>
<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')">
<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']);
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
</dt>
</div>
</span>
</tal:expanded>
</tal:group>
<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']);
title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');
id search/group"
class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a>
</dt>
</tal:searchOrGroup>

View file

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

View file

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

View file

@ -1,6 +1,12 @@
/* Appy-specific IE-fixes */
.portletSearch {
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. */

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<!codeHeader!>
import sys
import os, os.path, sys
try: # New CMF
from Products.CMFCore.permissions import setDefaultRoles
except ImportError: # Old CMF
@ -7,7 +7,7 @@ except ImportError: # Old CMF
import Extensions.appyWrappers
<!imports!>
# The following imports are here for allowing mixin classes to access those
# elements without being statically dependent on Plone/Zope packages. Indeed,
# every Archetype instance has a method "getProductConfig" that returns this
@ -23,6 +23,7 @@ logger = logging.getLogger('<!applicationName!>')
# Some global variables --------------------------------------------------------
PROJECTNAME = '<!applicationName!>'
diskFolder = os.path.dirname(<!applicationName!>.__file__)
defaultAddRoles = [<!defaultAddRoles!>]
DEFAULT_ADD_CONTENT_PERMISSION = "Add portal content"
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:
res += '%s,' % stringify(v)
res += ')'
elif value.__class__.__name__ == 'DateTime':
res = 'DateTime("%s")' % value.strftime('%Y/%m/%d %H:%M')
else:
res = str(value)
if isinstance(value, basestring):

View file

@ -6,6 +6,7 @@ import time, os.path, mimetypes, unicodedata
from appy.gen import Search
from appy.gen.utils import sequenceTypes
from appy.shared.utils import getOsTempFolder
from appy.shared.xml_parser import XmlMarshaller
# Some error messages ----------------------------------------------------------
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)
else:
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):
if other: return cmp(self.o, other.o)
else: return 1
@ -258,6 +261,26 @@ class AbstractWrapper:
method in those cases.'''
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:
'''When you get, from an appy object, the value of a File attribute, you