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

@ -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