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
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
27
gen/plone25/mixins/TestMixin.py
Normal file
27
gen/plone25/mixins/TestMixin.py
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
25
gen/plone25/templates/testAll.py
Normal file
25
gen/plone25/templates/testAll.py
Normal 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])
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue