Commit b255c894 authored by Julien Muchembled's avatar Julien Muchembled

PortalTransforms: merge upstream 2.0

This fixes test_20_reStructuredText partially.

Conflicts:
	Products/PortalTransforms/TransformEngine.py
	Products/PortalTransforms/libtransforms/commandtransform.py
	Products/PortalTransforms/transforms/safe_html.py
	Products/PortalTransforms/utils.py

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@41726 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent 4adafd42
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from logging import DEBUG from logging import DEBUG
from persistent.list import PersistentList
from zope.interface import implements from zope.interface import implements
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
from Acquisition import aq_base from Acquisition import aq_base
from App.class_init import default__class_init__ as InitializeClass from App.class_init import default__class_init__ as InitializeClass
from Persistence import PersistentMapping
try:
from ZODB.PersistentList import PersistentList
except ImportError:
from persistent.list import PersistentList
from OFS.Folder import Folder from OFS.Folder import Folder
from Persistence import PersistentMapping
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.CMFCore.ActionProviderBase import ActionProviderBase from Products.CMFCore.ActionProviderBase import ActionProviderBase
from Products.CMFCore.permissions import ManagePortal, View from Products.CMFCore.permissions import ManagePortal, View
try: try:
...@@ -22,18 +17,19 @@ except ImportError: # BACK: Zope 2.8 ...@@ -22,18 +17,19 @@ except ImportError: # BACK: Zope 2.8
registerToolInterface = lambda tool_id, tool_interface: None registerToolInterface = lambda tool_id, tool_interface: None
from Products.CMFCore.utils import UniqueObject from Products.CMFCore.utils import UniqueObject
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms import transforms
from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.interfaces import IEngine
from Products.PortalTransforms.interfaces import IPortalTransformsTool
from Products.PortalTransforms.data import datastream from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.chain import TransformsChain from Products.PortalTransforms.chain import TransformsChain
from Products.PortalTransforms.chain import chain from Products.PortalTransforms.chain import chain
from Products.PortalTransforms.cache import Cache from Products.PortalTransforms.cache import Cache
from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.interfaces import IEngine
from Products.PortalTransforms.interfaces import IPortalTransformsTool
from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms.Transform import Transform from Products.PortalTransforms.Transform import Transform
from Products.PortalTransforms.transforms import initialize
from Products.PortalTransforms.utils import log from Products.PortalTransforms.utils import log
from Products.PortalTransforms.utils import TransformException from Products.PortalTransforms.utils import TransformException
from Products.PortalTransforms.utils import _www from Products.PortalTransforms.utils import _www
...@@ -50,28 +46,25 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -50,28 +46,25 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
implements(IPortalTransformsTool, IEngine) implements(IPortalTransformsTool, IEngine)
meta_types = all_meta_types = ( meta_types = all_meta_types = (
{ 'name' : 'Transform', {'name': 'Transform', 'action': 'manage_addTransformForm'},
'action' : 'manage_addTransformForm'}, {'name': 'TransformsChain', 'action': 'manage_addTransformsChainForm'},
{ 'name' : 'TransformsChain',
'action' : 'manage_addTransformsChainForm'},
) )
manage_addTransformForm = PageTemplateFile('addTransform', _www) manage_addTransformForm = PageTemplateFile('addTransform', _www)
manage_addTransformsChainForm = PageTemplateFile('addTransformsChain', _www) manage_addTransformsChainForm = PageTemplateFile(
'addTransformsChain', _www)
manage_cacheForm = PageTemplateFile('setCacheTime', _www) manage_cacheForm = PageTemplateFile('setCacheTime', _www)
manage_editTransformationPolicyForm = PageTemplateFile('editTransformationPolicy', _www) manage_editTransformationPolicyForm = PageTemplateFile(
'editTransformationPolicy', _www)
manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www) manage_reloadAllTransforms = PageTemplateFile('reloadAllTransforms', _www)
manage_options = ((Folder.manage_options[0],) + Folder.manage_options[2:] + manage_options = (
( (Folder.manage_options[0], ) + Folder.manage_options[2:] +
{ 'label' : 'Caches', ({'label': 'Caches', 'action': 'manage_cacheForm'},
'action' : 'manage_cacheForm'}, {'label': 'Policy', 'action': 'manage_editTransformationPolicyForm'},
{ 'label' : 'Policy', {'label': 'Reload transforms',
'action' : 'manage_editTransformationPolicyForm'}, 'action': 'manage_reloadAllTransforms'},
{ 'label' : 'Reload transforms', ))
'action' : 'manage_reloadAllTransforms'},
)
)
security = ClassSecurityInfo() security = ClassSecurityInfo()
...@@ -81,7 +74,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -81,7 +74,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
self.max_sec_in_cache = max_sec_in_cache self.max_sec_in_cache = max_sec_in_cache
self._new_style_pt = 1 self._new_style_pt = 1
# mimetype oriented conversions (iengine interface) ######################## # mimetype oriented conversions (iengine interface)
def unregisterTransform(self, name): def unregisterTransform(self, name):
""" unregister a transform """ unregister a transform
...@@ -113,7 +106,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -113,7 +106,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
target_mimetype = str(target_mimetype) target_mimetype = str(target_mimetype)
if object is not None: if object is not None:
cache = Cache(object) cache = Cache(object, context=context)
data = cache.getCache(target_mimetype) data = cache.getCache(target_mimetype)
if data is not None: if data is not None:
time, data = data time, data = data
...@@ -126,8 +119,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -126,8 +119,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
registry = getToolByName(self, 'mimetypes_registry') registry = getToolByName(self, 'mimetypes_registry')
if not getattr(aq_base(registry), 'classify', None): if not getattr(aq_base(registry), 'classify', None):
# avoid problems when importing a site with an old mimetype registry # avoid problems when importing a site with an old mimetype
# XXX return None or orig? # registry
return None return None
orig_mt = registry.classify(orig, orig_mt = registry.classify(orig,
...@@ -135,8 +128,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -135,8 +128,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
filename=kwargs.get('filename')) filename=kwargs.get('filename'))
orig_mt = str(orig_mt) orig_mt = str(orig_mt)
if not orig_mt: if not orig_mt:
log('Unable to guess input mime type (filename=%s, mimetype=%s)' %( log('Unable to guess input mime type (filename=%s, mimetype=%s)' %
kwargs.get('mimetype'), kwargs.get('filename')), severity=WARNING) (kwargs.get('mimetype'), kwargs.get('filename')),
severity=WARNING)
return None return None
target_mt = registry.lookup(target_mimetype) target_mt = registry.lookup(target_mimetype)
...@@ -151,9 +145,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -151,9 +145,7 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
# If orig_mt and target_mt are the same, we only allow # If orig_mt and target_mt are the same, we only allow
# a one-hop transform, a.k.a. filter. # a one-hop transform, a.k.a. filter.
# XXX disabled filtering for now # XXX disabled filtering for now
filter_only = False
if orig_mt == str(target_mt): if orig_mt == str(target_mt):
filter_only = True
data.setData(orig) data.setData(orig)
md = data.getMetadata() md = data.getMetadata()
md['mimetype'] = str(orig_mt) md['mimetype'] = str(orig_mt)
...@@ -171,9 +163,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -171,9 +163,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
path = self._findPath(orig_mt, target_mt) path = self._findPath(orig_mt, target_mt)
if not path: if not path:
log('NO PATH FROM %s TO %s : %s' % (orig_mt, target_mimetype, path), log('NO PATH FROM %s TO %s : %s' %
severity=WARNING) (orig_mt, target_mimetype, path), severity=WARNING)
return None #XXX raise TransformError return None
if len(path) > 1: if len(path) > 1:
## create a chain on the fly (sly) ## create a chain on the fly (sly)
...@@ -183,7 +175,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -183,7 +175,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
else: else:
transform = path[0] transform = path[0]
result = transform.convert(orig, data, context=context, usedby=usedby, **kwargs) result = transform.convert(orig, data, context=context,
usedby=usedby, **kwargs)
self._setMetaData(result, transform) self._setMetaData(result, transform)
# set cache if possible # set cache if possible
...@@ -307,8 +300,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -307,8 +300,8 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
(output, transform.name()) (output, transform.name())
raise TransformException(msg) raise TransformException(msg)
if len(mto) > 1: if len(mto) > 1:
msg = 'Wildcarding not allowed in transform\'s output '\ msg = ("Wildcarding not allowed in transform's output "
'MIME type' "MIME type")
raise TransformException(msg) raise TransformException(msg)
for mt2 in mto[0].mimetypes: for mt2 in mto[0].mimetypes:
...@@ -342,36 +335,124 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -342,36 +335,124 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
"""return the shortest path for transformation from orig mimetype to """return the shortest path for transformation from orig mimetype to
target mimetype target mimetype
""" """
path = []
if not self._mtmap: if not self._mtmap:
return None return None
# naive algorithm : orig = str(orig)
# find all possible paths with required transforms target = str(target)
# take the shortest # First, let's deal with required transforms.
# if required_transforms:
# it should be enough since we should not have so much possible paths # Let's decompose paths, then.
shortest, winner = 9999, None required_transform = required_transforms.pop(0)
for path in self._getPaths(str(orig), str(target), required_transforms): # The first path must lead to one of the inputs supported
if len(path) < shortest: # by this first required transform.
winner = path # Which input types are supported by this transform ?
shortest = len(path) supportedInputs = {}
for input, outputs in self._mtmap.items():
for output, transforms in outputs.items():
for transform in transforms:
if transform.name() == required_transform:
supportedInputs[input] = 'ok'
# BTW, let's remember the output type
transformOutput = output
# and remember the transform, it is
# useful later
requiredTransform = transform
# Which of these inputs will be reachable with the
# shortest path ?
shortest = 9999 # big enough, I guess
shortestFirstPath = None
for supportedInput in supportedInputs.keys():
# We start from orig
firstOrig = orig
# And want to reach supportedInput
firstTarget = supportedInput
# What's the shortest path ?
firstPath = self._findPath(firstOrig, firstTarget)
if firstPath is not None:
if len(firstPath) < shortest:
# Here is a path which is shorter than others
# which also reach the required transform.
shortest = len(firstPath)
shortestFirstPath = firstPath
if shortestFirstPath == None:
return None # there is no path leading to this transform
# Then we have to take this transform.
secondPath = [requiredTransform]
# From the output of this transform, we then have to
# reach our target, possible through other required
# transforms.
thirdOrig = transformOutput
thirdTarget = target
thirdPath = self._findPath(thirdOrig, thirdTarget,
required_transforms)
if thirdPath is None:
return None # no path
# Final result is the concatenation of these 3 parts
return shortestFirstPath + secondPath + thirdPath
if orig == target:
return []
return winner # Now let's efficiently find the shortest path from orig
# to target (without required transforms).
# The overall idea is that we build all possible paths
# starting from orig and of given length. And we increment
# this length until one of these paths reaches our target or
# until all reachable types have been reached.
currentPathLength = 0
pathToType = {orig: []} # all paths we know, by end of path.
def typesWithPathOfLength(length):
'''Returns the lists of known paths of a given length'''
result = []
for type_, path in pathToType.items():
if len(path) == length:
result.append(type_)
return result
# We will start exploring paths which start from types
# reachable in zero steps. That is paths which start from
# orig.
typesToStartFrom = typesWithPathOfLength(currentPathLength)
# Explore paths while there are new paths to be explored
while len(typesToStartFrom) > 0:
for startingType in typesToStartFrom:
# Where can we go in one step starting from here ?
outputs = self._mtmap.get(startingType)
if outputs:
for reachedType, transforms in outputs.items():
# Does this lead to a type we never reached before ?
if reachedType not in pathToType.keys() and transforms:
# Yes, we did not know any path reaching this type
# Let's remember the path to here
pathToType[reachedType] = (
pathToType[startingType] + [transforms[0]])
if reachedType == target:
# This is the first time we reach our target.
# We have our shortest path to target.
return pathToType[target]
# We explored all possible paths of length currentPathLength
# Let's increment that length.
currentPathLength += 1
# What are the next types to start from ?
typesToStartFrom = typesWithPathOfLength(currentPathLength)
# We are done exploring paths starting from orig
# and this exploration did not reach our target.
# Hence there is no path from orig to target.
return None
def _getPaths(self, orig, target, requirements, path=None, result=None, searched_orig_list=None): def _getPaths(self, orig, target, requirements, path=None, result=None):
"""return a all path for transformation from orig mimetype to """return some of the paths for transformation from orig mimetype to
target mimetype target mimetype with the guarantee that the shortest path is included.
If target is the same as orig, then returns an empty path.
""" """
# don't search the same orig again, otherwise infinite loop occurs.
if searched_orig_list is None:
searched_orig_list = []
if orig in searched_orig_list:
return result
else:
searched_orig_list.append(orig)
shortest = 9999
if result:
for okPath in result:
shortest = min(shortest, len(okPath))
if orig == target:
return [[]]
if path is None: if path is None:
result = [] result = []
path = [] path = []
...@@ -400,8 +481,14 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -400,8 +481,14 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
if o_mt in target_aliases: if o_mt in target_aliases:
if not requirements: if not requirements:
result.append(path[:]) result.append(path[:])
if len(path[:]) < shortest:
# here is a shorter one !
shortest = len(path)
else: else:
self._getPaths(o_mt, target, requirements, path, result, searched_orig_list) if len(path) < shortest:
# keep exploring this path, it is still short enough
self._getPaths(o_mt, target, requirements,
path, result)
if required: if required:
requirements.append(name) requirements.append(name)
path.pop() path.pop()
...@@ -414,14 +501,11 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -414,14 +501,11 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
transform tool is added transform tool is added
""" """
Folder.manage_afterAdd(self, item, container) Folder.manage_afterAdd(self, item, container)
transforms.initialize(self) try:
# XXX required? initialize(self)
#try: except TransformException:
# # first initialization # may fail on copy or zexp import
# transforms.initialize(self) pass
#except:
# # may fail on copy
# pass
security.declareProtected(ManagePortal, 'manage_addTransform') security.declareProtected(ManagePortal, 'manage_addTransform')
def manage_addTransform(self, id, module, REQUEST=None): def manage_addTransform(self, id, module, REQUEST=None):
...@@ -465,28 +549,31 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -465,28 +549,31 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
reloaded.append((id, o.module)) reloaded.append((id, o.module))
return reloaded return reloaded
# Policy handling methods ################################################# # Policy handling methods
def manage_addPolicy(self, output_mimetype, required_transforms, REQUEST=None): def manage_addPolicy(self, output_mimetype, required_transforms,
REQUEST=None):
""" add a policy for a given output mime types""" """ add a policy for a given output mime types"""
registry = getToolByName(self, 'mimetypes_registry') registry = getToolByName(self, 'mimetypes_registry')
if not registry.lookup(output_mimetype): if not registry.lookup(output_mimetype):
raise TransformException('Unknown MIME type') raise TransformException('Unknown MIME type')
if self._policies.has_key(output_mimetype): if output_mimetype in self._policies:
msg = 'A policy for output %s is yet defined' % output_mimetype msg = 'A policy for output %s is yet defined' % output_mimetype
raise TransformException(msg) raise TransformException(msg)
required_transforms = tuple(required_transforms) required_transforms = tuple(required_transforms)
self._policies[output_mimetype] = required_transforms self._policies[output_mimetype] = required_transforms
if REQUEST is not None: if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') REQUEST['RESPONSE'].redirect(self.absolute_url() +
'/manage_editTransformationPolicyForm')
def manage_delPolicies(self, outputs, REQUEST=None): def manage_delPolicies(self, outputs, REQUEST=None):
""" remove policies for given output mime types""" """ remove policies for given output mime types"""
for mimetype in outputs: for mimetype in outputs:
del self._policies[mimetype] del self._policies[mimetype]
if REQUEST is not None: if REQUEST is not None:
REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_editTransformationPolicyForm') REQUEST['RESPONSE'].redirect(self.absolute_url() +
'/manage_editTransformationPolicyForm')
def listPolicies(self): def listPolicies(self):
""" return the list of defined policies """ return the list of defined policies
...@@ -498,20 +585,21 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -498,20 +585,21 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
self._policies = PersistentMapping() self._policies = PersistentMapping()
return self._policies.items() return self._policies.items()
# mimetype oriented conversions (iengine interface) ######################## # mimetype oriented conversions (iengine interface)
def registerTransform(self, transform): def registerTransform(self, transform):
"""register a new transform """register a new transform
transform isn't a Zope Transform (the wrapper) but the wrapped transform transform isn't a Zope Transform (the wrapper) but the wrapped
the persistence wrapper will be created here transform the persistence wrapper will be created here
""" """
# needed when call from transform.transforms.initialize which # needed when call from transform.transforms.initialize which
# register non zope transform # register non zope transform
module = str(transform.__module__) module = str(transform.__module__)
transform = Transform(transform.name(), module, transform) transform = Transform(transform.name(), module, transform)
if not ITransform.providedBy(transform): if not ITransform.providedBy(transform):
raise TransformException('%s does not implement ITransform' % transform) raise TransformException('%s does not implement ITransform' %
transform)
name = transform.name() name = transform.name()
__traceback_info__ = (name, transform) __traceback_info__ = (name, transform)
if name not in self.objectIds(): if name not in self.objectIds():
...@@ -539,8 +627,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder): ...@@ -539,8 +627,9 @@ class TransformTool(UniqueObject, ActionProviderBase, Folder):
# available mimetypes #################################################### # available mimetypes ####################################################
def listAvailableTextInputs(self): def listAvailableTextInputs(self):
""" Returns a list of mimetypes that can be used as input for textfields """Returns a list of mimetypes that can be used as input for textfields
by building a list of the inputs beginning with "text/" of all transforms. by building a list of the inputs beginning with "text/" of all
transforms.
""" """
available_types = [] available_types = []
candidate_transforms = [object[1] for object in self.objectItems()] candidate_transforms = [object[1] for object in self.objectItems()]
......
...@@ -3,9 +3,15 @@ ...@@ -3,9 +3,15 @@
from time import time from time import time
from Acquisition import aq_base from Acquisition import aq_base
_marker = object()
class Cache: class Cache:
def __init__(self, context, _id='_v_transform_cache'): def __init__(self, obj, context=None, _id='_v_transform_cache'):
self.obj = obj
if context is None:
self.context = obj
else:
self.context = context self.context = context
self._id =_id self._id =_id
...@@ -17,17 +23,19 @@ class Cache: ...@@ -17,17 +23,19 @@ class Cache:
key = key.replace('+', '_') key = key.replace('+', '_')
key = key.replace('-', '_') key = key.replace('-', '_')
key = key.replace(' ', '_') key = key.replace(' ', '_')
if hasattr(aq_base(self.context), 'absolute_url'):
return key, self.context.absolute_url()
return key return key
def setCache(self, key, value): def setCache(self, key, value):
"""cache a value indexed by key""" """cache a value indexed by key"""
if not value.isCacheable(): if not value.isCacheable():
return return
context = self.context obj = self.obj
key = self._genCacheKey(key) key = self._genCacheKey(key)
if getattr(aq_base(context), self._id, None) is None: if getattr(aq_base(obj), self._id, None) is None:
setattr(context, self._id, {}) setattr(obj, self._id, {})
getattr(context, self._id)[key] = (time(), value) getattr(obj, self._id)[key] = (time(), value)
return key return key
def getCache(self, key): def getCache(self, key):
...@@ -36,9 +44,9 @@ class Cache: ...@@ -36,9 +44,9 @@ class Cache:
return None if not present return None if not present
else return a tuple (time spent in cache, value) else return a tuple (time spent in cache, value)
""" """
context = self.context obj = self.obj
key = self._genCacheKey(key) key = self._genCacheKey(key)
dict = getattr(context, self._id, None) dict = getattr(obj, self._id, None)
if dict is None : if dict is None :
return None return None
try: try:
...@@ -50,14 +58,14 @@ class Cache: ...@@ -50,14 +58,14 @@ class Cache:
def purgeCache(self, key=None): def purgeCache(self, key=None):
"""Remove cache """Remove cache
""" """
context = self.context obj = self.obj
id = self._id id = self._id
if not shasattr(context, id): if getattr(obj, id, _marker) is _marker:
return return
if key is None: if key is None:
delattr(context, id) delattr(obj, id)
else: else:
cache = getattr(context, id) cache = getattr(obj, id)
key = self._genCacheKey(key) key = self._genCacheKey(key)
if cache.has_key(key): if cache.has_key(key):
del cache[key] del cache[key]
...@@ -87,6 +87,8 @@ class popentransform: ...@@ -87,6 +87,8 @@ class popentransform:
def convert(self, data, cache, **kwargs): def convert(self, data, cache, **kwargs):
command = "%s %s" % (self.binary, self.binaryArgs) command = "%s %s" % (self.binary, self.binaryArgs)
tmpname = None
try:
if not self.useStdin: if not self.useStdin:
tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp tmpfile, tmpname = tempfile.mkstemp(text=False) # create tmp
os.write(tmpfile, data) # write data to tmp using a file descriptor os.write(tmpfile, data) # write data to tmp using a file descriptor
...@@ -103,12 +105,12 @@ class popentransform: ...@@ -103,12 +105,12 @@ class popentransform:
out = self.getData(couterr) out = self.getData(couterr)
couterr.close() couterr.close()
if not self.useStdin:
# remove tmp file
os.unlink(tmpname)
cache.setData(out) cache.setData(out)
return cache return cache
finally:
if not self.useStdin and tmpname is not None:
# remove tmp file
os.unlink(tmpname)
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import shlex import shlex
......
import re import re
import os import os
import sys import sys
from sgmllib import SGMLParser from sgmllib import SGMLParser, SGMLParseError
try: try:
# Need to be imported before win32api to avoid dll loading # Need to be imported before win32api to avoid dll loading
...@@ -208,6 +208,25 @@ class StrippingParser( SGMLParser ): ...@@ -208,6 +208,25 @@ class StrippingParser( SGMLParser ):
self.result = "%s</%s>" % (self.result, tag) self.result = "%s</%s>" % (self.result, tag)
remTag = '</%s>' % tag remTag = '</%s>' % tag
def parse_declaration(self, i):
"""Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
"""
j = None
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1:
k = len(self.rawdata)
data = self.rawdata[i+9:k]
j = k+3
self.result.append("<![CDATA[%s]]>" % data)
else:
try:
j = SGMLParser.parse_declaration(self, i)
except SGMLParseError:
toHandle = self.rawdata[i:]
self.result.append(toHandle)
j = i + len(toHandle)
return j
def scrubHTML( html ): def scrubHTML( html ):
""" Strip illegal HTML tags from string text. """ """ Strip illegal HTML tags from string text. """
......
## Testing Markdown ## Testing Markdown
`code` and _italic_ and *bold* and even a [link](http://plone.org). `code` and _italic_ and *bold* and even a [link](http://plone.org).
Fööbär
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
</tr> </tr>
</table> </table>
<p>This is a text used as a blind text.</p> <p>This is a text used as a blind text.</p>
<div><![CDATA[
Some CDATA text.
]]>
</div>
<ul> <ul>
<li>A sample list item1</li> <li>A sample list item1</li>
<li>A sample list item2</li> <li>A sample list item2</li>
......
<A name=1></a>Chapter 44<br> <A name=1></a>Chapter&nbsp;44<br>Writing&nbsp;Basic&nbsp;Unit&nbsp;Tests<br>Difficulty<br>Newcomer<br>Skills<br>&nbsp;All&nbsp;you&nbsp;need&nbsp;to&nbsp;know&nbsp;is&nbsp;some&nbsp;Python.<br>Problem/Task<br>As&nbsp;you&nbsp;know&nbsp;by&nbsp;now,&nbsp;Zope&nbsp;3&nbsp;gains&nbsp;its&nbsp;incredible&nbsp;stability&nbsp;from&nbsp;testing&nbsp;any&nbsp;code&nbsp;in&nbsp;great&nbsp;detail.&nbsp;The<br>currently&nbsp;most&nbsp;common&nbsp;method&nbsp;is&nbsp;to&nbsp;write&nbsp;unit&nbsp;tests.&nbsp;This&nbsp;chapter&nbsp;introduces&nbsp;unit&nbsp;tests&nbsp;&nbsp;which<br>are&nbsp;Zope&nbsp;3&nbsp;independent&nbsp;&nbsp;and&nbsp;introduces&nbsp;some&nbsp;of&nbsp;the&nbsp;subtleties.<br>Solution<br>44.1<br>Implementing&nbsp;the&nbsp;Sample&nbsp;Class<br>Before&nbsp;we&nbsp;can&nbsp;write&nbsp;tests,&nbsp;we&nbsp;have&nbsp;to&nbsp;write&nbsp;some&nbsp;code&nbsp;that&nbsp;we&nbsp;can&nbsp;test.&nbsp;Here,&nbsp;we&nbsp;will&nbsp;implement<br>a&nbsp;simple&nbsp;class&nbsp;called&nbsp;Sample&nbsp;with&nbsp;a&nbsp;public&nbsp;attribute&nbsp;title&nbsp;and&nbsp;description&nbsp;that&nbsp;is&nbsp;accessed<br>via&nbsp;getDescription()&nbsp;and&nbsp;mutated&nbsp;using&nbsp;setDescription().&nbsp;Further,&nbsp;the&nbsp;description&nbsp;must&nbsp;be<br>either&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string.<br>Since&nbsp;this&nbsp;code&nbsp;will&nbsp;not&nbsp;depend&nbsp;on&nbsp;Zope,&nbsp;open&nbsp;a&nbsp;file&nbsp;named&nbsp;test&nbsp;sample.py&nbsp;anywhere&nbsp;and&nbsp;add<br>the&nbsp;following&nbsp;class:<br>1&nbsp;Sample(object):<br>2<br>&quot;&quot;&quot;A&nbsp;trivial&nbsp;Sample&nbsp;object.&quot;&quot;&quot;<br>3<br>4<br>title&nbsp;=&nbsp;None<br>5<br>6<br>def&nbsp;__init__(self):<br>7<br>&quot;&quot;&quot;Initialize&nbsp;object.&quot;&quot;&quot;<br>8<br>self._description&nbsp;=&nbsp;’’<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>10<br>def&nbsp;setDescription(self,&nbsp;value):<br>11<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>12<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>13<br>self._description&nbsp;=&nbsp;value<br>14<br>15<br>def&nbsp;getDescription(self):<br>16<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>17<br>return&nbsp;self._description<br>Line&nbsp;4:&nbsp;The&nbsp;title&nbsp;is&nbsp;just&nbsp;publicly&nbsp;declared&nbsp;and&nbsp;a&nbsp;value&nbsp;of&nbsp;None&nbsp;is&nbsp;given.&nbsp;Therefore&nbsp;this&nbsp;is&nbsp;just<br>a&nbsp;regular&nbsp;attribute.<br>Line&nbsp;8:&nbsp;The&nbsp;actual&nbsp;description&nbsp;string&nbsp;will&nbsp;be&nbsp;stored&nbsp;in&nbsp;description.<br>Line&nbsp;12:&nbsp;Make&nbsp;sure&nbsp;that&nbsp;the&nbsp;description&nbsp;is&nbsp;only&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string,&nbsp;like&nbsp;it&nbsp;was&nbsp;stated&nbsp;in<br>the&nbsp;requirements.<br>If&nbsp;you&nbsp;wish&nbsp;you&nbsp;can&nbsp;now&nbsp;manually&nbsp;test&nbsp;the&nbsp;class&nbsp;with&nbsp;the&nbsp;interactive&nbsp;Python&nbsp;shell.&nbsp;Just&nbsp;start<br>Python&nbsp;by&nbsp;entering&nbsp;python&nbsp;in&nbsp;your&nbsp;shell&nbsp;prompt.&nbsp;Note&nbsp;that&nbsp;you&nbsp;should&nbsp;be&nbsp;in&nbsp;the&nbsp;directory&nbsp;in<br>which&nbsp;test&nbsp;sample.py&nbsp;is&nbsp;located&nbsp;when&nbsp;starting&nbsp;Python&nbsp;(an&nbsp;alternative&nbsp;is&nbsp;of&nbsp;course&nbsp;to&nbsp;specify&nbsp;the<br>directory&nbsp;in&nbsp;your&nbsp;PYTHONPATH.)<br>1&nbsp;&gt;&gt;&gt;&nbsp;from&nbsp;test_sample&nbsp;import&nbsp;Sample<br>2&nbsp;&gt;&gt;&gt;&nbsp;sample&nbsp;=&nbsp;Sample()<br>3&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>4&nbsp;None<br>5&nbsp;&gt;&gt;&gt;&nbsp;sample.title&nbsp;=&nbsp;’Title’<br>6&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>7&nbsp;Title<br>8&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>9<br>10&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(’Hello&nbsp;World’)<br>11&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>12&nbsp;Hello&nbsp;World<br>13&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(None)<br>14&nbsp;Traceback&nbsp;(most&nbsp;recent&nbsp;call&nbsp;last):<br>15<br>File&nbsp;&quot;&lt;stdin&gt;&quot;,&nbsp;line&nbsp;1,&nbsp;in&nbsp;?<br>16<br>File&nbsp;&quot;test_sample.py&quot;,&nbsp;line&nbsp;31,&nbsp;in&nbsp;setDescription<br>17<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>18&nbsp;AssertionError<br>As&nbsp;you&nbsp;can&nbsp;see&nbsp;in&nbsp;the&nbsp;last&nbsp;test,&nbsp;non-string&nbsp;object&nbsp;types&nbsp;are&nbsp;not&nbsp;allowed&nbsp;as&nbsp;descriptions&nbsp;and&nbsp;an<br>AssertionError&nbsp;is&nbsp;raised.<br>44.2<br>Writing&nbsp;the&nbsp;Unit&nbsp;Tests<br>The&nbsp;goal&nbsp;of&nbsp;writing&nbsp;the&nbsp;unit&nbsp;tests&nbsp;is&nbsp;to&nbsp;convert&nbsp;this&nbsp;informal,&nbsp;manual,&nbsp;and&nbsp;interactive&nbsp;testing&nbsp;session<br>into&nbsp;a&nbsp;formal&nbsp;test&nbsp;class.&nbsp;Python&nbsp;provides&nbsp;already&nbsp;a&nbsp;module&nbsp;called&nbsp;unittest&nbsp;for&nbsp;this&nbsp;purpose,&nbsp;which<br>is&nbsp;a&nbsp;port&nbsp;of&nbsp;the&nbsp;Java-based&nbsp;unit&nbsp;testing&nbsp;product,&nbsp;JUnit,&nbsp;by&nbsp;Kent&nbsp;Beck&nbsp;and&nbsp;Erich&nbsp;Gamma.&nbsp;There&nbsp;are<br>three&nbsp;levels&nbsp;to&nbsp;the&nbsp;testing&nbsp;framework&nbsp;(this&nbsp;list&nbsp;deviates&nbsp;a&nbsp;bit&nbsp;from&nbsp;the&nbsp;original&nbsp;definitions&nbsp;as&nbsp;found<br>in&nbsp;the&nbsp;Python&nbsp;library&nbsp;documentation.&nbsp;1).<br>1&nbsp;http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2.&nbsp;WRITING&nbsp;THE&nbsp;UNIT&nbsp;TESTS<br>3<br>The&nbsp;smallest&nbsp;unit&nbsp;is&nbsp;obviously&nbsp;the&nbsp;“test”,&nbsp;which&nbsp;is&nbsp;a&nbsp;single&nbsp;method&nbsp;in&nbsp;a&nbsp;TestCase&nbsp;class&nbsp;that<br>tests&nbsp;the&nbsp;behavior&nbsp;of&nbsp;a&nbsp;small&nbsp;piece&nbsp;of&nbsp;code&nbsp;or&nbsp;a&nbsp;particular&nbsp;aspect&nbsp;of&nbsp;an&nbsp;implementation.&nbsp;The&nbsp;“test<br>case”&nbsp;is&nbsp;then&nbsp;a&nbsp;collection&nbsp;tests&nbsp;that&nbsp;share&nbsp;the&nbsp;same&nbsp;setup/inputs.&nbsp;On&nbsp;top&nbsp;of&nbsp;all&nbsp;of&nbsp;this&nbsp;sits&nbsp;the&nbsp;“test<br>suite”&nbsp;which&nbsp;is&nbsp;a&nbsp;collection&nbsp;of&nbsp;test&nbsp;cases&nbsp;and/or&nbsp;other&nbsp;test&nbsp;suites.&nbsp;Test&nbsp;suites&nbsp;combine&nbsp;tests&nbsp;that<br>should&nbsp;be&nbsp;executed&nbsp;together.&nbsp;With&nbsp;the&nbsp;correct&nbsp;setup&nbsp;(as&nbsp;shown&nbsp;in&nbsp;the&nbsp;example&nbsp;below),&nbsp;you&nbsp;can<br>then&nbsp;execute&nbsp;test&nbsp;suites.&nbsp;For&nbsp;large&nbsp;projects&nbsp;like&nbsp;Zope&nbsp;3,&nbsp;it&nbsp;is&nbsp;useful&nbsp;to&nbsp;know&nbsp;that&nbsp;there&nbsp;is&nbsp;also&nbsp;the<br>concept&nbsp;of&nbsp;a&nbsp;test&nbsp;runner,&nbsp;which&nbsp;manages&nbsp;the&nbsp;test&nbsp;run&nbsp;of&nbsp;all&nbsp;or&nbsp;a&nbsp;set&nbsp;of&nbsp;tests.&nbsp;The&nbsp;runner&nbsp;provides<br>useful&nbsp;feedback&nbsp;to&nbsp;the&nbsp;application,&nbsp;so&nbsp;that&nbsp;various&nbsp;user&nbsp;interaces&nbsp;can&nbsp;be&nbsp;developed&nbsp;on&nbsp;top&nbsp;of&nbsp;it.<br>But&nbsp;enough&nbsp;about&nbsp;the&nbsp;theory.&nbsp;In&nbsp;the&nbsp;following&nbsp;example,&nbsp;which&nbsp;you&nbsp;can&nbsp;simply&nbsp;put&nbsp;into&nbsp;the&nbsp;same<br>file&nbsp;as&nbsp;your&nbsp;code&nbsp;above,&nbsp;you&nbsp;will&nbsp;see&nbsp;a&nbsp;test&nbsp;in&nbsp;common&nbsp;Zope&nbsp;3&nbsp;style.<br>1&nbsp;import&nbsp;unittest<br>2<br>3&nbsp;class&nbsp;SampleTest(unittest.TestCase):<br>4<br>&quot;&quot;&quot;Test&nbsp;the&nbsp;Sample&nbsp;class&quot;&quot;&quot;<br>5<br>6<br>def&nbsp;test_title(self):<br>7<br>sample&nbsp;=&nbsp;Sample()<br>8<br>self.assertEqual(sample.title,&nbsp;None)<br>9<br>sample.title&nbsp;=&nbsp;’Sample&nbsp;Title’<br>10<br>self.assertEqual(sample.title,&nbsp;’Sample&nbsp;Title’)<br>11<br>12<br>def&nbsp;test_getDescription(self):<br>13<br>sample&nbsp;=&nbsp;Sample()<br>14<br>self.assertEqual(sample.getDescription(),&nbsp;’’)<br>15<br>sample._description&nbsp;=&nbsp;&quot;Description&quot;<br>16<br>self.assertEqual(sample.getDescription(),&nbsp;’Description’)<br>17<br>18<br>def&nbsp;test_setDescription(self):<br>19<br>sample&nbsp;=&nbsp;Sample()<br>20<br>self.assertEqual(sample._description,&nbsp;’’)<br>21<br>sample.setDescription(’Description’)<br>22<br>self.assertEqual(sample._description,&nbsp;’Description’)<br>23<br>sample.setDescription(u’Description2’)<br>24<br>self.assertEqual(sample._description,&nbsp;u’Description2’)<br>25<br>self.assertRaises(AssertionError,&nbsp;sample.setDescription,&nbsp;None)<br>26<br>27<br>28&nbsp;def&nbsp;test_suite():<br>29<br>return&nbsp;unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33&nbsp;if&nbsp;__name__&nbsp;==&nbsp;’__main__’:<br>34<br>unittest.main(defaultTest=’test_suite’)<br>Line&nbsp;3–4:&nbsp;We&nbsp;usually&nbsp;develop&nbsp;test&nbsp;classes&nbsp;which&nbsp;must&nbsp;inherit&nbsp;from&nbsp;TestCase.&nbsp;While&nbsp;often&nbsp;not<br>done,&nbsp;it&nbsp;is&nbsp;a&nbsp;good&nbsp;idea&nbsp;to&nbsp;give&nbsp;the&nbsp;class&nbsp;a&nbsp;meaningful&nbsp;docstring&nbsp;that&nbsp;describes&nbsp;the&nbsp;purpose&nbsp;of&nbsp;the<br>tests&nbsp;it&nbsp;includes.<br>Line&nbsp;6,&nbsp;12&nbsp;&amp;&nbsp;18:&nbsp;When&nbsp;a&nbsp;test&nbsp;case&nbsp;is&nbsp;run,&nbsp;a&nbsp;method&nbsp;called&nbsp;runTests()&nbsp;is&nbsp;executed.&nbsp;While&nbsp;it<br>is&nbsp;possible&nbsp;to&nbsp;overrride&nbsp;this&nbsp;method&nbsp;to&nbsp;run&nbsp;tests&nbsp;differently,&nbsp;the&nbsp;default&nbsp;option&nbsp;will&nbsp;look&nbsp;for&nbsp;any<br>method&nbsp;whose&nbsp;name&nbsp;starts&nbsp;with&nbsp;test&nbsp;and&nbsp;execute&nbsp;it&nbsp;as&nbsp;a&nbsp;single&nbsp;test.&nbsp;This&nbsp;way&nbsp;we&nbsp;can&nbsp;create<br>a&nbsp;“test&nbsp;method”&nbsp;for&nbsp;each&nbsp;aspect,&nbsp;method,&nbsp;function&nbsp;or&nbsp;property&nbsp;of&nbsp;the&nbsp;code&nbsp;to&nbsp;be&nbsp;tested.&nbsp;This<br>default&nbsp;is&nbsp;very&nbsp;sensible&nbsp;and&nbsp;is&nbsp;used&nbsp;everywhere&nbsp;in&nbsp;Zope&nbsp;3.<br><hr><A name=4></a>4<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>Note&nbsp;that&nbsp;there&nbsp;is&nbsp;no&nbsp;docstring&nbsp;for&nbsp;test&nbsp;methods.&nbsp;This&nbsp;is&nbsp;intentional.&nbsp;If&nbsp;a&nbsp;docstring&nbsp;is&nbsp;specified,<br>it&nbsp;is&nbsp;used&nbsp;instead&nbsp;of&nbsp;the&nbsp;method&nbsp;name&nbsp;to&nbsp;identify&nbsp;the&nbsp;test.&nbsp;When&nbsp;specifying&nbsp;a&nbsp;docstring,&nbsp;we&nbsp;have<br>noticed&nbsp;that&nbsp;it&nbsp;is&nbsp;very&nbsp;difficult&nbsp;to&nbsp;identify&nbsp;the&nbsp;test&nbsp;later;&nbsp;therefore&nbsp;the&nbsp;method&nbsp;name&nbsp;is&nbsp;a&nbsp;much<br>better&nbsp;choice.<br>Line&nbsp;8,&nbsp;10,&nbsp;14,&nbsp;.&nbsp;.&nbsp;.&nbsp;:&nbsp;The&nbsp;TestCase&nbsp;class&nbsp;implements&nbsp;a&nbsp;handful&nbsp;of&nbsp;methods&nbsp;that&nbsp;aid&nbsp;you&nbsp;with&nbsp;the<br>testing.&nbsp;Here&nbsp;are&nbsp;some&nbsp;of&nbsp;the&nbsp;most&nbsp;frequently&nbsp;used&nbsp;ones.&nbsp;For&nbsp;a&nbsp;complete&nbsp;list&nbsp;see&nbsp;the&nbsp;standard<br>Python&nbsp;documentation&nbsp;referenced&nbsp;above.<br>&nbsp;assertEqual(first,second[,msg])<br>Checks&nbsp;whether&nbsp;the&nbsp;first&nbsp;and&nbsp;second&nbsp;value&nbsp;are&nbsp;equal.&nbsp;If&nbsp;the&nbsp;test&nbsp;fails,&nbsp;the&nbsp;msg&nbsp;or&nbsp;None<br>is&nbsp;returned.<br>&nbsp;assertNotEqual(first,second[,msg])<br>This&nbsp;is&nbsp;simply&nbsp;the&nbsp;opposite&nbsp;to&nbsp;assertEqual()&nbsp;by&nbsp;checking&nbsp;for&nbsp;non-equality.<br>&nbsp;assertRaises(exception,callable,...)<br>You&nbsp;expect&nbsp;the&nbsp;callable&nbsp;to&nbsp;raise&nbsp;exception&nbsp;when&nbsp;executed.&nbsp;After&nbsp;the&nbsp;callable&nbsp;you&nbsp;can<br>specify&nbsp;any&nbsp;amount&nbsp;of&nbsp;positional&nbsp;and&nbsp;keyword&nbsp;arguments&nbsp;for&nbsp;the&nbsp;callable.&nbsp;If&nbsp;you&nbsp;expect<br>a&nbsp;group&nbsp;of&nbsp;exceptions&nbsp;from&nbsp;the&nbsp;execution,&nbsp;you&nbsp;can&nbsp;make&nbsp;exception&nbsp;a&nbsp;tuple&nbsp;of&nbsp;possible<br>exceptions.<br>&nbsp;assert&nbsp;(expr[,msg])<br>Assert&nbsp;checks&nbsp;whether&nbsp;the&nbsp;specified&nbsp;expression&nbsp;executes&nbsp;correctly.&nbsp;If&nbsp;not,&nbsp;the&nbsp;test&nbsp;fails&nbsp;and<br>msg&nbsp;or&nbsp;None&nbsp;is&nbsp;returned.<br>&nbsp;failUnlessEqual()<br>This&nbsp;testing&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assertEqual().<br>&nbsp;failUnless(expr[,msg])<br>This&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assert&nbsp;(expr[,msg]).<br>&nbsp;failif()<br>This&nbsp;is&nbsp;the&nbsp;opposite&nbsp;to&nbsp;failUnless().<br>&nbsp;fail([msg])<br>Fails&nbsp;the&nbsp;running&nbsp;test&nbsp;without&nbsp;any&nbsp;evaluation.&nbsp;This&nbsp;is&nbsp;commonly&nbsp;used&nbsp;when&nbsp;testing&nbsp;various<br>possible&nbsp;execution&nbsp;paths&nbsp;at&nbsp;once&nbsp;and&nbsp;you&nbsp;would&nbsp;like&nbsp;to&nbsp;signify&nbsp;a&nbsp;failure&nbsp;if&nbsp;an&nbsp;improper&nbsp;path<br>was&nbsp;taken.<br>Line&nbsp;6–10:&nbsp;This&nbsp;method&nbsp;tests&nbsp;the&nbsp;title&nbsp;attribute&nbsp;of&nbsp;the&nbsp;Sample&nbsp;class.&nbsp;The&nbsp;first&nbsp;test&nbsp;should<br>be&nbsp;of&nbsp;course&nbsp;that&nbsp;the&nbsp;attribute&nbsp;exists&nbsp;and&nbsp;has&nbsp;the&nbsp;expected&nbsp;initial&nbsp;value&nbsp;(line&nbsp;8).&nbsp;Then&nbsp;the&nbsp;title<br>attribute&nbsp;is&nbsp;changed&nbsp;and&nbsp;we&nbsp;check&nbsp;whether&nbsp;the&nbsp;value&nbsp;was&nbsp;really&nbsp;stored.&nbsp;This&nbsp;might&nbsp;seem&nbsp;like<br>overkill,&nbsp;but&nbsp;later&nbsp;you&nbsp;might&nbsp;change&nbsp;the&nbsp;title&nbsp;in&nbsp;a&nbsp;way&nbsp;that&nbsp;it&nbsp;uses&nbsp;properties&nbsp;instead.&nbsp;Then&nbsp;it<br>becomes&nbsp;very&nbsp;important&nbsp;to&nbsp;check&nbsp;whether&nbsp;this&nbsp;test&nbsp;still&nbsp;passes.<br>Line&nbsp;12–16:&nbsp;First&nbsp;we&nbsp;simply&nbsp;check&nbsp;that&nbsp;getDescription()&nbsp;returns&nbsp;the&nbsp;correct&nbsp;default&nbsp;value.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;want&nbsp;to&nbsp;use&nbsp;other&nbsp;API&nbsp;calls&nbsp;like&nbsp;setDescription()&nbsp;we&nbsp;set&nbsp;a&nbsp;new&nbsp;value&nbsp;of&nbsp;the<br>description&nbsp;via&nbsp;the&nbsp;implementation-internal&nbsp;description&nbsp;attribute&nbsp;(line&nbsp;15).&nbsp;This&nbsp;is&nbsp;okay!&nbsp;Unit<br>tests&nbsp;can&nbsp;make&nbsp;use&nbsp;of&nbsp;implementation-specific&nbsp;attributes&nbsp;and&nbsp;methods.&nbsp;Finally&nbsp;we&nbsp;just&nbsp;check&nbsp;that<br>the&nbsp;correct&nbsp;value&nbsp;is&nbsp;returned.<br><hr><A name=5></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>5<br>Line&nbsp;18–25:&nbsp;On&nbsp;line&nbsp;21–24&nbsp;it&nbsp;is&nbsp;checked&nbsp;that&nbsp;both&nbsp;regular&nbsp;and&nbsp;unicode&nbsp;strings&nbsp;are&nbsp;set&nbsp;correctly.<br>In&nbsp;the&nbsp;last&nbsp;line&nbsp;of&nbsp;the&nbsp;test&nbsp;we&nbsp;make&nbsp;sure&nbsp;that&nbsp;no&nbsp;other&nbsp;type&nbsp;of&nbsp;objects&nbsp;can&nbsp;be&nbsp;set&nbsp;as&nbsp;a&nbsp;description<br>and&nbsp;that&nbsp;an&nbsp;error&nbsp;is&nbsp;raised.<br>28–31:&nbsp;This&nbsp;method&nbsp;returns&nbsp;a&nbsp;test&nbsp;suite&nbsp;that&nbsp;includes&nbsp;all&nbsp;test&nbsp;cases&nbsp;created&nbsp;in&nbsp;this&nbsp;module.&nbsp;It&nbsp;is<br>used&nbsp;by&nbsp;the&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner&nbsp;when&nbsp;it&nbsp;picks&nbsp;up&nbsp;all&nbsp;available&nbsp;tests.&nbsp;You&nbsp;would&nbsp;basically&nbsp;add&nbsp;the<br>line&nbsp;unittest.makeSuite(TestCaseClass)&nbsp;for&nbsp;each&nbsp;additional&nbsp;test&nbsp;case.<br>33–34:&nbsp;In&nbsp;order&nbsp;to&nbsp;make&nbsp;the&nbsp;test&nbsp;module&nbsp;runnable&nbsp;by&nbsp;itself,&nbsp;you&nbsp;can&nbsp;execute&nbsp;unittest.main()<br>when&nbsp;the&nbsp;module&nbsp;is&nbsp;run.<br>44.3<br>Running&nbsp;the&nbsp;Tests<br>You&nbsp;can&nbsp;run&nbsp;the&nbsp;test&nbsp;by&nbsp;simply&nbsp;calling&nbsp;pythontest&nbsp;sample.py&nbsp;from&nbsp;the&nbsp;directory&nbsp;you&nbsp;saved&nbsp;the<br>file&nbsp;in.&nbsp;Here&nbsp;is&nbsp;the&nbsp;result&nbsp;you&nbsp;should&nbsp;see:<br>.<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.001s<br>The&nbsp;three&nbsp;dots&nbsp;represent&nbsp;the&nbsp;three&nbsp;tests&nbsp;that&nbsp;were&nbsp;run.&nbsp;If&nbsp;a&nbsp;test&nbsp;had&nbsp;failed,&nbsp;it&nbsp;would&nbsp;have&nbsp;been<br>reported&nbsp;pointing&nbsp;out&nbsp;the&nbsp;failing&nbsp;test&nbsp;and&nbsp;providing&nbsp;a&nbsp;small&nbsp;traceback.<br>When&nbsp;using&nbsp;the&nbsp;default&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner,&nbsp;tests&nbsp;will&nbsp;be&nbsp;picked&nbsp;up&nbsp;as&nbsp;long&nbsp;as&nbsp;they&nbsp;follow&nbsp;some<br>conventions.<br>&nbsp;The&nbsp;tests&nbsp;must&nbsp;either&nbsp;be&nbsp;in&nbsp;a&nbsp;package&nbsp;or&nbsp;be&nbsp;a&nbsp;module&nbsp;called&nbsp;tests.<br>&nbsp;If&nbsp;tests&nbsp;is&nbsp;a&nbsp;package,&nbsp;then&nbsp;all&nbsp;test&nbsp;modules&nbsp;inside&nbsp;must&nbsp;also&nbsp;have&nbsp;a&nbsp;name&nbsp;starting&nbsp;with&nbsp;test,<br>as&nbsp;it&nbsp;is&nbsp;the&nbsp;case&nbsp;with&nbsp;our&nbsp;name&nbsp;test&nbsp;sample.py.<br>&nbsp;The&nbsp;test&nbsp;module&nbsp;must&nbsp;be&nbsp;somewhere&nbsp;in&nbsp;the&nbsp;Zope&nbsp;3&nbsp;source&nbsp;tree,&nbsp;since&nbsp;the&nbsp;test&nbsp;runner&nbsp;looks<br>only&nbsp;for&nbsp;files&nbsp;there.<br>In&nbsp;our&nbsp;case,&nbsp;you&nbsp;could&nbsp;simply&nbsp;create&nbsp;a&nbsp;tests&nbsp;package&nbsp;in&nbsp;ZOPE3/src&nbsp;(do&nbsp;not&nbsp;forget&nbsp;the<br>init&nbsp;.<br>py&nbsp;file).&nbsp;Then&nbsp;place&nbsp;the&nbsp;test&nbsp;sample.py&nbsp;file&nbsp;into&nbsp;this&nbsp;directory.<br>You&nbsp;you&nbsp;can&nbsp;use&nbsp;the&nbsp;test&nbsp;runner&nbsp;to&nbsp;run&nbsp;only&nbsp;the&nbsp;sample&nbsp;tests&nbsp;as&nbsp;follows&nbsp;from&nbsp;the&nbsp;Zope&nbsp;3&nbsp;root<br>directory:<br>python&nbsp;test.py&nbsp;-vp&nbsp;tests.test_sample<br>The&nbsp;-v&nbsp;option&nbsp;stands&nbsp;for&nbsp;verbose&nbsp;mode,&nbsp;so&nbsp;that&nbsp;detailed&nbsp;information&nbsp;about&nbsp;a&nbsp;test&nbsp;failure&nbsp;is<br>provided.&nbsp;The&nbsp;-p&nbsp;option&nbsp;enables&nbsp;a&nbsp;progress&nbsp;bar&nbsp;that&nbsp;tells&nbsp;you&nbsp;how&nbsp;many&nbsp;tests&nbsp;out&nbsp;of&nbsp;all&nbsp;have&nbsp;been<br>completed.&nbsp;There&nbsp;are&nbsp;many&nbsp;more&nbsp;options&nbsp;that&nbsp;can&nbsp;be&nbsp;specified.&nbsp;You&nbsp;can&nbsp;get&nbsp;a&nbsp;full&nbsp;list&nbsp;of&nbsp;them&nbsp;with<br>the&nbsp;option&nbsp;-h:&nbsp;pythontest.py-h.<br>The&nbsp;output&nbsp;of&nbsp;the&nbsp;call&nbsp;above&nbsp;is&nbsp;as&nbsp;follows:<br>nfiguration&nbsp;file&nbsp;found.<br>nning&nbsp;UNIT&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;UNIT&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>3/3&nbsp;(100.0%):&nbsp;test_title&nbsp;(tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.002s<br><hr><A name=6></a>6<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>--------------------------------------------------------------------<br>n&nbsp;0&nbsp;tests&nbsp;in&nbsp;0.000s<br>Line&nbsp;1:&nbsp;The&nbsp;test&nbsp;runner&nbsp;uses&nbsp;a&nbsp;configuration&nbsp;file&nbsp;for&nbsp;some&nbsp;setup.&nbsp;This&nbsp;allows&nbsp;developers&nbsp;to&nbsp;use<br>the&nbsp;test&nbsp;runner&nbsp;for&nbsp;other&nbsp;projects&nbsp;as&nbsp;well.&nbsp;This&nbsp;message&nbsp;simply&nbsp;tells&nbsp;us&nbsp;that&nbsp;the&nbsp;configuration&nbsp;file<br>was&nbsp;found.<br>Line&nbsp;2–8:&nbsp;The&nbsp;unit&nbsp;tests&nbsp;are&nbsp;run.&nbsp;On&nbsp;line&nbsp;4&nbsp;you&nbsp;can&nbsp;see&nbsp;the&nbsp;progress&nbsp;bar.<br>Line&nbsp;9–15:&nbsp;The&nbsp;functional&nbsp;tests&nbsp;are&nbsp;run,&nbsp;since&nbsp;the&nbsp;default&nbsp;test&nbsp;runner&nbsp;runs&nbsp;both&nbsp;types&nbsp;of&nbsp;tests.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;have&nbsp;any&nbsp;functional&nbsp;tests&nbsp;in&nbsp;the&nbsp;specified&nbsp;module,&nbsp;there&nbsp;are&nbsp;no&nbsp;tests&nbsp;to&nbsp;run.&nbsp;To<br>just&nbsp;run&nbsp;the&nbsp;unit&nbsp;tests,&nbsp;use&nbsp;option&nbsp;-u&nbsp;and&nbsp;-f&nbsp;for&nbsp;just&nbsp;running&nbsp;the&nbsp;functional&nbsp;tests.&nbsp;See&nbsp;“Writing<br>Functional&nbsp;Tests”&nbsp;for&nbsp;more&nbsp;detials&nbsp;on&nbsp;functional&nbsp;tests.<br><hr><A name=7></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>7<br>Exercises<br>1.&nbsp;It&nbsp;is&nbsp;not&nbsp;very&nbsp;common&nbsp;to&nbsp;do&nbsp;the&nbsp;setup&nbsp;&nbsp;in&nbsp;our&nbsp;case&nbsp;sample=Sample()&nbsp;&nbsp;in&nbsp;every&nbsp;test<br>method.&nbsp;Instead&nbsp;there&nbsp;exists&nbsp;a&nbsp;method&nbsp;called&nbsp;setUp()&nbsp;and&nbsp;its&nbsp;counterpart&nbsp;tearDown&nbsp;that<br>are&nbsp;run&nbsp;before&nbsp;and&nbsp;after&nbsp;each&nbsp;test,&nbsp;respectively.&nbsp;Change&nbsp;the&nbsp;test&nbsp;code&nbsp;above,&nbsp;so&nbsp;that&nbsp;it&nbsp;uses<br>the&nbsp;setUp()&nbsp;method.&nbsp;In&nbsp;later&nbsp;chapters&nbsp;and&nbsp;the&nbsp;rest&nbsp;of&nbsp;the&nbsp;book&nbsp;we&nbsp;will&nbsp;frequently&nbsp;use&nbsp;this<br>method&nbsp;of&nbsp;setting&nbsp;up&nbsp;tests.<br>2.&nbsp;Currently&nbsp;the&nbsp;test&nbsp;setDescription()&nbsp;test&nbsp;only&nbsp;verifies&nbsp;that&nbsp;None&nbsp;is&nbsp;not&nbsp;allowed&nbsp;as&nbsp;input<br>value.<br>(a)&nbsp;Improve&nbsp;the&nbsp;test,&nbsp;so&nbsp;that&nbsp;all&nbsp;other&nbsp;builtin&nbsp;types&nbsp;are&nbsp;tested&nbsp;as&nbsp;well.<br>(b)&nbsp;Also,&nbsp;make&nbsp;sure&nbsp;that&nbsp;any&nbsp;objects&nbsp;inheriting&nbsp;from&nbsp;str&nbsp;or&nbsp;unicode&nbsp;pass&nbsp;as&nbsp;valid&nbsp;values.<br><hr>
Writing Basic Unit Tests<br> \ No newline at end of file
Difficulty<br>
Newcomer<br>
Skills<br>
• All you need to know is some Python.<br>
Problem/Task<br>
As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests – which<br>are Zope 3 independent – and introduces some of the subtleties.<br>
Solution<br>
44.1<br>
Implementing the Sample Class<br>
Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>
Since this code will not depend on Zope, open a file named test sample.py anywhere and add<br>
the following class:<br>
1 Sample(object):<br>
2<br>
&quot;&quot;&quot;A trivial Sample object.&quot;&quot;&quot;<br>
3<br>
4<br>
title = None<br>
5<br>
6<br>
def __init__(self):<br>
7<br>
&quot;&quot;&quot;Initialize object.&quot;&quot;&quot;<br>
8<br>
self._description = ’’<br>
9<br>
1<br>
<hr>
<A name=2></a>2<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
10<br>
def setDescription(self, value):<br>
11<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
12<br>
assert isinstance(value, (str, unicode))<br>
13<br>
self._description = value<br>
14<br>
15<br>
def getDescription(self):<br>
16<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
17<br>
return self._description<br>
Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>
Line 8: The actual description string will be stored in description.<br>
Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>
If you wish you can now manually test the class with the interactive Python shell. Just start<br>
Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>
1 &gt;&gt;&gt; from test_sample import Sample<br>2 &gt;&gt;&gt; sample = Sample()<br>
3 &gt;&gt;&gt; print sample.title<br>4 None<br>
5 &gt;&gt;&gt; sample.title = ’Title’<br>
6 &gt;&gt;&gt; print sample.title<br>7 Title<br>
8 &gt;&gt;&gt; print sample.getDescription()<br>9<br>
10 &gt;&gt;&gt; sample.setDescription(’Hello World’)<br>
11 &gt;&gt;&gt; print sample.getDescription()<br>12 Hello World<br>
13 &gt;&gt;&gt; sample.setDescription(None)<br>
14 Traceback (most recent call last):<br>
15<br>
File &quot;&lt;stdin&gt;&quot;, line 1, in ?<br>
16<br>
File &quot;test_sample.py&quot;, line 31, in setDescription<br>
17<br>
assert isinstance(value, (str, unicode))<br>
18 AssertionError<br>
As you can see in the last test, non-string object types are not allowed as descriptions and an<br>
AssertionError is raised.<br>
44.2<br>
Writing the Unit Tests<br>
The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original definitions as found<br>in the Python library documentation. 1).<br>
1 http://www.python.org/doc/current/lib/module-unittest.html<br>
<hr>
<A name=3></a>44.2. WRITING THE UNIT TESTS<br>
3<br>
The smallest unit is obviously the “test”, which is a single method in a TestCase class that<br>
tests the behavior of a small piece of code or a particular aspect of an implementation. The “test<br>case” is then a collection tests that share the same setup/inputs. On top of all of this sits the “test<br>suite” which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>
But enough about the theory. In the following example, which you can simply put into the same<br>
file as your code above, you will see a test in common Zope 3 style.<br>
1 import unittest<br>2<br>
3 class SampleTest(unittest.TestCase):<br>4<br>
&quot;&quot;&quot;Test the Sample class&quot;&quot;&quot;<br>
5<br>
6<br>
def test_title(self):<br>
7<br>
sample = Sample()<br>
8<br>
self.assertEqual(sample.title, None)<br>
9<br>
sample.title = ’Sample Title’<br>
10<br>
self.assertEqual(sample.title, ’Sample Title’)<br>
11<br>
12<br>
def test_getDescription(self):<br>
13<br>
sample = Sample()<br>
14<br>
self.assertEqual(sample.getDescription(), ’’)<br>
15<br>
sample._description = &quot;Description&quot;<br>
16<br>
self.assertEqual(sample.getDescription(), ’Description’)<br>
17<br>
18<br>
def test_setDescription(self):<br>
19<br>
sample = Sample()<br>
20<br>
self.assertEqual(sample._description, ’’)<br>
21<br>
sample.setDescription(’Description’)<br>
22<br>
self.assertEqual(sample._description, ’Description’)<br>
23<br>
sample.setDescription(u’Description2’)<br>
24<br>
self.assertEqual(sample._description, u’Description2’)<br>
25<br>
self.assertRaises(AssertionError, sample.setDescription, None)<br>
26<br>
27<br>
28 def test_suite():<br>29<br>
return unittest.TestSuite((<br>
30<br>
unittest.makeSuite(SampleTest),<br>
31<br>
))<br>
32<br>
33 if __name__ == ’__main__’:<br>34<br>
unittest.main(defaultTest=’test_suite’)<br>
Line 3–4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>
Line 6, 12 &amp; 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests differently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a “test method” for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br>
<hr>
<A name=4></a>4<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
Note that there is no docstring for test methods. This is intentional. If a docstring is specified,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very difficult to identify the test later; therefore the method name is a much<br>better choice.<br>
Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>
• assertEqual(first,second[,msg])<br>
Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>
• assertNotEqual(first,second[,msg])<br>
This is simply the opposite to assertEqual() by checking for non-equality.<br>
• assertRaises(exception,callable,...)<br>
You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>
• assert (expr[,msg])<br>
Assert checks whether the specified expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>
• failUnlessEqual()<br>
This testing method is equivalent to assertEqual().<br>
• failUnless(expr[,msg])<br>
This method is equivalent to assert (expr[,msg]).<br>
• failif()<br>
This is the opposite to failUnless().<br>
• fail([msg])<br>
Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>
Line 6–10: This method tests the title attribute of the Sample class. The first test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>
Line 12–16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-specific attributes and methods. Finally we just check that<br>the correct value is returned.<br>
<hr>
<A name=5></a>44.3. RUNNING THE TESTS<br>
5<br>
Line 18–25: On line 21–24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>
28–31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>
33–34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>
44.3<br>
Running the Tests<br>
You can run the test by simply calling pythontest sample.py from the directory you saved the<br>file in. Here is the result you should see:<br>
.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>
The three dots represent the three tests that were run. If a test had failed, it would have been<br>
reported pointing out the failing test and providing a small traceback.<br>
When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>
conventions.<br>
• The tests must either be in a package or be a module called tests.<br>
• If tests is a package, then all test modules inside must also have a name starting with test,<br>
as it is the case with our name test sample.py.<br>
• The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>
only for files there.<br>
In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>
init .<br>
py file). Then place the test sample.py file into this directory.<br>
You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>
directory:<br>
python test.py -vp tests.test_sample<br>
The -v option stands for verbose mode, so that detailed information about a test failure is<br>
provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be specified. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>
The output of the call above is as follows:<br>
nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>
3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>
--------------------------------------------------------------------<br>n 3 tests in 0.002s<br>
<hr>
<A name=6></a>6<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>
--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>
Line 1: The test runner uses a configuration file for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the configuration file<br>was found.<br>
Line 2–8: The unit tests are run. On line 4 you can see the progress bar.<br>
Line 9–15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the specified module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See “Writing<br>Functional Tests” for more detials on functional tests.<br>
<hr>
<A name=7></a>44.3. RUNNING THE TESTS<br>
7<br>
Exercises<br>
1. It is not very common to do the setup – in our case sample=Sample() – in every test<br>
method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>
2. Currently the test setDescription() test only verifies that None is not allowed as input<br>
value.<br>
(a) Improve the test, so that all other builtin types are tested as well.<br>
(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br>
<hr>
\ No newline at end of file
<A name=1></a>Chapter 44<br> <A name=1></a>Chapter&nbsp;44<br>Writing&nbsp;Basic&nbsp;Unit&nbsp;Tests<br>Difficulty<br>Newcomer<br>Skills<br>•&nbsp;All&nbsp;you&nbsp;need&nbsp;to&nbsp;know&nbsp;is&nbsp;some&nbsp;Python.<br>Problem/Task<br>As&nbsp;you&nbsp;know&nbsp;by&nbsp;now,&nbsp;Zope&nbsp;3&nbsp;gains&nbsp;its&nbsp;incredible&nbsp;stability&nbsp;from&nbsp;testing&nbsp;any&nbsp;code&nbsp;in&nbsp;great&nbsp;detail.&nbsp;The<br>currently&nbsp;most&nbsp;common&nbsp;method&nbsp;is&nbsp;to&nbsp;write&nbsp;unit&nbsp;tests.&nbsp;This&nbsp;chapter&nbsp;introduces&nbsp;unit&nbsp;tests&nbsp;–&nbsp;which<br>are&nbsp;Zope&nbsp;3&nbsp;independent&nbsp;–&nbsp;and&nbsp;introduces&nbsp;some&nbsp;of&nbsp;the&nbsp;subtleties.<br>Solution<br>44.1<br>Implementing&nbsp;the&nbsp;Sample&nbsp;Class<br>Before&nbsp;we&nbsp;can&nbsp;write&nbsp;tests,&nbsp;we&nbsp;have&nbsp;to&nbsp;write&nbsp;some&nbsp;code&nbsp;that&nbsp;we&nbsp;can&nbsp;test.&nbsp;Here,&nbsp;we&nbsp;will&nbsp;implement<br>a&nbsp;simple&nbsp;class&nbsp;called&nbsp;Sample&nbsp;with&nbsp;a&nbsp;public&nbsp;attribute&nbsp;title&nbsp;and&nbsp;description&nbsp;that&nbsp;is&nbsp;accessed<br>via&nbsp;getDescription()&nbsp;and&nbsp;mutated&nbsp;using&nbsp;setDescription().&nbsp;Further,&nbsp;the&nbsp;description&nbsp;must&nbsp;be<br>either&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string.<br>Since&nbsp;this&nbsp;code&nbsp;will&nbsp;not&nbsp;depend&nbsp;on&nbsp;Zope,&nbsp;open&nbsp;a&nbsp;file&nbsp;named&nbsp;test&nbsp;sample.py&nbsp;anywhere&nbsp;and&nbsp;add<br>the&nbsp;following&nbsp;class:<br>1&nbsp;Sample(object):<br>2<br>&quot;&quot;&quot;A&nbsp;trivial&nbsp;Sample&nbsp;object.&quot;&quot;&quot;<br>3<br>4<br>title&nbsp;=&nbsp;None<br>5<br>6<br>def&nbsp;__init__(self):<br>7<br>&quot;&quot;&quot;Initialize&nbsp;object.&quot;&quot;&quot;<br>8<br>self._description&nbsp;=&nbsp;’’<br>9<br>1<br><hr><A name=2></a>2<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>10<br>def&nbsp;setDescription(self,&nbsp;value):<br>11<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>12<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>13<br>self._description&nbsp;=&nbsp;value<br>14<br>15<br>def&nbsp;getDescription(self):<br>16<br>&quot;&quot;&quot;Change&nbsp;the&nbsp;value&nbsp;of&nbsp;the&nbsp;description.&quot;&quot;&quot;<br>17<br>return&nbsp;self._description<br>Line&nbsp;4:&nbsp;The&nbsp;title&nbsp;is&nbsp;just&nbsp;publicly&nbsp;declared&nbsp;and&nbsp;a&nbsp;value&nbsp;of&nbsp;None&nbsp;is&nbsp;given.&nbsp;Therefore&nbsp;this&nbsp;is&nbsp;just<br>a&nbsp;regular&nbsp;attribute.<br>Line&nbsp;8:&nbsp;The&nbsp;actual&nbsp;description&nbsp;string&nbsp;will&nbsp;be&nbsp;stored&nbsp;in&nbsp;description.<br>Line&nbsp;12:&nbsp;Make&nbsp;sure&nbsp;that&nbsp;the&nbsp;description&nbsp;is&nbsp;only&nbsp;a&nbsp;regular&nbsp;or&nbsp;unicode&nbsp;string,&nbsp;like&nbsp;it&nbsp;was&nbsp;stated&nbsp;in<br>the&nbsp;requirements.<br>If&nbsp;you&nbsp;wish&nbsp;you&nbsp;can&nbsp;now&nbsp;manually&nbsp;test&nbsp;the&nbsp;class&nbsp;with&nbsp;the&nbsp;interactive&nbsp;Python&nbsp;shell.&nbsp;Just&nbsp;start<br>Python&nbsp;by&nbsp;entering&nbsp;python&nbsp;in&nbsp;your&nbsp;shell&nbsp;prompt.&nbsp;Note&nbsp;that&nbsp;you&nbsp;should&nbsp;be&nbsp;in&nbsp;the&nbsp;directory&nbsp;in<br>which&nbsp;test&nbsp;sample.py&nbsp;is&nbsp;located&nbsp;when&nbsp;starting&nbsp;Python&nbsp;(an&nbsp;alternative&nbsp;is&nbsp;of&nbsp;course&nbsp;to&nbsp;specify&nbsp;the<br>directory&nbsp;in&nbsp;your&nbsp;PYTHONPATH.)<br>1&nbsp;&gt;&gt;&gt;&nbsp;from&nbsp;test_sample&nbsp;import&nbsp;Sample<br>2&nbsp;&gt;&gt;&gt;&nbsp;sample&nbsp;=&nbsp;Sample()<br>3&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>4&nbsp;None<br>5&nbsp;&gt;&gt;&gt;&nbsp;sample.title&nbsp;=&nbsp;’Title’<br>6&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.title<br>7&nbsp;Title<br>8&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>9<br>10&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(’Hello&nbsp;World’)<br>11&nbsp;&gt;&gt;&gt;&nbsp;print&nbsp;sample.getDescription()<br>12&nbsp;Hello&nbsp;World<br>13&nbsp;&gt;&gt;&gt;&nbsp;sample.setDescription(None)<br>14&nbsp;Traceback&nbsp;(most&nbsp;recent&nbsp;call&nbsp;last):<br>15<br>File&nbsp;&quot;&lt;stdin&gt;&quot;,&nbsp;line&nbsp;1,&nbsp;in&nbsp;?<br>16<br>File&nbsp;&quot;test_sample.py&quot;,&nbsp;line&nbsp;31,&nbsp;in&nbsp;setDescription<br>17<br>assert&nbsp;isinstance(value,&nbsp;(str,&nbsp;unicode))<br>18&nbsp;AssertionError<br>As&nbsp;you&nbsp;can&nbsp;see&nbsp;in&nbsp;the&nbsp;last&nbsp;test,&nbsp;non-string&nbsp;object&nbsp;types&nbsp;are&nbsp;not&nbsp;allowed&nbsp;as&nbsp;descriptions&nbsp;and&nbsp;an<br>AssertionError&nbsp;is&nbsp;raised.<br>44.2<br>Writing&nbsp;the&nbsp;Unit&nbsp;Tests<br>The&nbsp;goal&nbsp;of&nbsp;writing&nbsp;the&nbsp;unit&nbsp;tests&nbsp;is&nbsp;to&nbsp;convert&nbsp;this&nbsp;informal,&nbsp;manual,&nbsp;and&nbsp;interactive&nbsp;testing&nbsp;session<br>into&nbsp;a&nbsp;formal&nbsp;test&nbsp;class.&nbsp;Python&nbsp;provides&nbsp;already&nbsp;a&nbsp;module&nbsp;called&nbsp;unittest&nbsp;for&nbsp;this&nbsp;purpose,&nbsp;which<br>is&nbsp;a&nbsp;port&nbsp;of&nbsp;the&nbsp;Java-based&nbsp;unit&nbsp;testing&nbsp;product,&nbsp;JUnit,&nbsp;by&nbsp;Kent&nbsp;Beck&nbsp;and&nbsp;Erich&nbsp;Gamma.&nbsp;There&nbsp;are<br>three&nbsp;levels&nbsp;to&nbsp;the&nbsp;testing&nbsp;framework&nbsp;(this&nbsp;list&nbsp;deviates&nbsp;a&nbsp;bit&nbsp;from&nbsp;the&nbsp;original&nbsp;definitions&nbsp;as&nbsp;found<br>in&nbsp;the&nbsp;Python&nbsp;library&nbsp;documentation.&nbsp;1).<br>1&nbsp;http://www.python.org/doc/current/lib/module-unittest.html<br><hr><A name=3></a>44.2.&nbsp;WRITING&nbsp;THE&nbsp;UNIT&nbsp;TESTS<br>3<br>The&nbsp;smallest&nbsp;unit&nbsp;is&nbsp;obviously&nbsp;the&nbsp;“test”,&nbsp;which&nbsp;is&nbsp;a&nbsp;single&nbsp;method&nbsp;in&nbsp;a&nbsp;TestCase&nbsp;class&nbsp;that<br>tests&nbsp;the&nbsp;behavior&nbsp;of&nbsp;a&nbsp;small&nbsp;piece&nbsp;of&nbsp;code&nbsp;or&nbsp;a&nbsp;particular&nbsp;aspect&nbsp;of&nbsp;an&nbsp;implementation.&nbsp;The&nbsp;“test<br>case”&nbsp;is&nbsp;then&nbsp;a&nbsp;collection&nbsp;tests&nbsp;that&nbsp;share&nbsp;the&nbsp;same&nbsp;setup/inputs.&nbsp;On&nbsp;top&nbsp;of&nbsp;all&nbsp;of&nbsp;this&nbsp;sits&nbsp;the&nbsp;“test<br>suite”&nbsp;which&nbsp;is&nbsp;a&nbsp;collection&nbsp;of&nbsp;test&nbsp;cases&nbsp;and/or&nbsp;other&nbsp;test&nbsp;suites.&nbsp;Test&nbsp;suites&nbsp;combine&nbsp;tests&nbsp;that<br>should&nbsp;be&nbsp;executed&nbsp;together.&nbsp;With&nbsp;the&nbsp;correct&nbsp;setup&nbsp;(as&nbsp;shown&nbsp;in&nbsp;the&nbsp;example&nbsp;below),&nbsp;you&nbsp;can<br>then&nbsp;execute&nbsp;test&nbsp;suites.&nbsp;For&nbsp;large&nbsp;projects&nbsp;like&nbsp;Zope&nbsp;3,&nbsp;it&nbsp;is&nbsp;useful&nbsp;to&nbsp;know&nbsp;that&nbsp;there&nbsp;is&nbsp;also&nbsp;the<br>concept&nbsp;of&nbsp;a&nbsp;test&nbsp;runner,&nbsp;which&nbsp;manages&nbsp;the&nbsp;test&nbsp;run&nbsp;of&nbsp;all&nbsp;or&nbsp;a&nbsp;set&nbsp;of&nbsp;tests.&nbsp;The&nbsp;runner&nbsp;provides<br>useful&nbsp;feedback&nbsp;to&nbsp;the&nbsp;application,&nbsp;so&nbsp;that&nbsp;various&nbsp;user&nbsp;interaces&nbsp;can&nbsp;be&nbsp;developed&nbsp;on&nbsp;top&nbsp;of&nbsp;it.<br>But&nbsp;enough&nbsp;about&nbsp;the&nbsp;theory.&nbsp;In&nbsp;the&nbsp;following&nbsp;example,&nbsp;which&nbsp;you&nbsp;can&nbsp;simply&nbsp;put&nbsp;into&nbsp;the&nbsp;same<br>file&nbsp;as&nbsp;your&nbsp;code&nbsp;above,&nbsp;you&nbsp;will&nbsp;see&nbsp;a&nbsp;test&nbsp;in&nbsp;common&nbsp;Zope&nbsp;3&nbsp;style.<br>1&nbsp;import&nbsp;unittest<br>2<br>3&nbsp;class&nbsp;SampleTest(unittest.TestCase):<br>4<br>&quot;&quot;&quot;Test&nbsp;the&nbsp;Sample&nbsp;class&quot;&quot;&quot;<br>5<br>6<br>def&nbsp;test_title(self):<br>7<br>sample&nbsp;=&nbsp;Sample()<br>8<br>self.assertEqual(sample.title,&nbsp;None)<br>9<br>sample.title&nbsp;=&nbsp;’Sample&nbsp;Title’<br>10<br>self.assertEqual(sample.title,&nbsp;’Sample&nbsp;Title’)<br>11<br>12<br>def&nbsp;test_getDescription(self):<br>13<br>sample&nbsp;=&nbsp;Sample()<br>14<br>self.assertEqual(sample.getDescription(),&nbsp;’’)<br>15<br>sample._description&nbsp;=&nbsp;&quot;Description&quot;<br>16<br>self.assertEqual(sample.getDescription(),&nbsp;’Description’)<br>17<br>18<br>def&nbsp;test_setDescription(self):<br>19<br>sample&nbsp;=&nbsp;Sample()<br>20<br>self.assertEqual(sample._description,&nbsp;’’)<br>21<br>sample.setDescription(’Description’)<br>22<br>self.assertEqual(sample._description,&nbsp;’Description’)<br>23<br>sample.setDescription(u’Description2’)<br>24<br>self.assertEqual(sample._description,&nbsp;u’Description2’)<br>25<br>self.assertRaises(AssertionError,&nbsp;sample.setDescription,&nbsp;None)<br>26<br>27<br>28&nbsp;def&nbsp;test_suite():<br>29<br>return&nbsp;unittest.TestSuite((<br>30<br>unittest.makeSuite(SampleTest),<br>31<br>))<br>32<br>33&nbsp;if&nbsp;__name__&nbsp;==&nbsp;’__main__’:<br>34<br>unittest.main(defaultTest=’test_suite’)<br>Line&nbsp;3–4:&nbsp;We&nbsp;usually&nbsp;develop&nbsp;test&nbsp;classes&nbsp;which&nbsp;must&nbsp;inherit&nbsp;from&nbsp;TestCase.&nbsp;While&nbsp;often&nbsp;not<br>done,&nbsp;it&nbsp;is&nbsp;a&nbsp;good&nbsp;idea&nbsp;to&nbsp;give&nbsp;the&nbsp;class&nbsp;a&nbsp;meaningful&nbsp;docstring&nbsp;that&nbsp;describes&nbsp;the&nbsp;purpose&nbsp;of&nbsp;the<br>tests&nbsp;it&nbsp;includes.<br>Line&nbsp;6,&nbsp;12&nbsp;&amp;&nbsp;18:&nbsp;When&nbsp;a&nbsp;test&nbsp;case&nbsp;is&nbsp;run,&nbsp;a&nbsp;method&nbsp;called&nbsp;runTests()&nbsp;is&nbsp;executed.&nbsp;While&nbsp;it<br>is&nbsp;possible&nbsp;to&nbsp;overrride&nbsp;this&nbsp;method&nbsp;to&nbsp;run&nbsp;tests&nbsp;differently,&nbsp;the&nbsp;default&nbsp;option&nbsp;will&nbsp;look&nbsp;for&nbsp;any<br>method&nbsp;whose&nbsp;name&nbsp;starts&nbsp;with&nbsp;test&nbsp;and&nbsp;execute&nbsp;it&nbsp;as&nbsp;a&nbsp;single&nbsp;test.&nbsp;This&nbsp;way&nbsp;we&nbsp;can&nbsp;create<br>a&nbsp;“test&nbsp;method”&nbsp;for&nbsp;each&nbsp;aspect,&nbsp;method,&nbsp;function&nbsp;or&nbsp;property&nbsp;of&nbsp;the&nbsp;code&nbsp;to&nbsp;be&nbsp;tested.&nbsp;This<br>default&nbsp;is&nbsp;very&nbsp;sensible&nbsp;and&nbsp;is&nbsp;used&nbsp;everywhere&nbsp;in&nbsp;Zope&nbsp;3.<br><hr><A name=4></a>4<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>Note&nbsp;that&nbsp;there&nbsp;is&nbsp;no&nbsp;docstring&nbsp;for&nbsp;test&nbsp;methods.&nbsp;This&nbsp;is&nbsp;intentional.&nbsp;If&nbsp;a&nbsp;docstring&nbsp;is&nbsp;specified,<br>it&nbsp;is&nbsp;used&nbsp;instead&nbsp;of&nbsp;the&nbsp;method&nbsp;name&nbsp;to&nbsp;identify&nbsp;the&nbsp;test.&nbsp;When&nbsp;specifying&nbsp;a&nbsp;docstring,&nbsp;we&nbsp;have<br>noticed&nbsp;that&nbsp;it&nbsp;is&nbsp;very&nbsp;difficult&nbsp;to&nbsp;identify&nbsp;the&nbsp;test&nbsp;later;&nbsp;therefore&nbsp;the&nbsp;method&nbsp;name&nbsp;is&nbsp;a&nbsp;much<br>better&nbsp;choice.<br>Line&nbsp;8,&nbsp;10,&nbsp;14,&nbsp;.&nbsp;.&nbsp;.&nbsp;:&nbsp;The&nbsp;TestCase&nbsp;class&nbsp;implements&nbsp;a&nbsp;handful&nbsp;of&nbsp;methods&nbsp;that&nbsp;aid&nbsp;you&nbsp;with&nbsp;the<br>testing.&nbsp;Here&nbsp;are&nbsp;some&nbsp;of&nbsp;the&nbsp;most&nbsp;frequently&nbsp;used&nbsp;ones.&nbsp;For&nbsp;a&nbsp;complete&nbsp;list&nbsp;see&nbsp;the&nbsp;standard<br>Python&nbsp;documentation&nbsp;referenced&nbsp;above.<br>•&nbsp;assertEqual(first,second[,msg])<br>Checks&nbsp;whether&nbsp;the&nbsp;first&nbsp;and&nbsp;second&nbsp;value&nbsp;are&nbsp;equal.&nbsp;If&nbsp;the&nbsp;test&nbsp;fails,&nbsp;the&nbsp;msg&nbsp;or&nbsp;None<br>is&nbsp;returned.<br>•&nbsp;assertNotEqual(first,second[,msg])<br>This&nbsp;is&nbsp;simply&nbsp;the&nbsp;opposite&nbsp;to&nbsp;assertEqual()&nbsp;by&nbsp;checking&nbsp;for&nbsp;non-equality.<br>•&nbsp;assertRaises(exception,callable,...)<br>You&nbsp;expect&nbsp;the&nbsp;callable&nbsp;to&nbsp;raise&nbsp;exception&nbsp;when&nbsp;executed.&nbsp;After&nbsp;the&nbsp;callable&nbsp;you&nbsp;can<br>specify&nbsp;any&nbsp;amount&nbsp;of&nbsp;positional&nbsp;and&nbsp;keyword&nbsp;arguments&nbsp;for&nbsp;the&nbsp;callable.&nbsp;If&nbsp;you&nbsp;expect<br>a&nbsp;group&nbsp;of&nbsp;exceptions&nbsp;from&nbsp;the&nbsp;execution,&nbsp;you&nbsp;can&nbsp;make&nbsp;exception&nbsp;a&nbsp;tuple&nbsp;of&nbsp;possible<br>exceptions.<br>•&nbsp;assert&nbsp;(expr[,msg])<br>Assert&nbsp;checks&nbsp;whether&nbsp;the&nbsp;specified&nbsp;expression&nbsp;executes&nbsp;correctly.&nbsp;If&nbsp;not,&nbsp;the&nbsp;test&nbsp;fails&nbsp;and<br>msg&nbsp;or&nbsp;None&nbsp;is&nbsp;returned.<br>•&nbsp;failUnlessEqual()<br>This&nbsp;testing&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assertEqual().<br>•&nbsp;failUnless(expr[,msg])<br>This&nbsp;method&nbsp;is&nbsp;equivalent&nbsp;to&nbsp;assert&nbsp;(expr[,msg]).<br>•&nbsp;failif()<br>This&nbsp;is&nbsp;the&nbsp;opposite&nbsp;to&nbsp;failUnless().<br>•&nbsp;fail([msg])<br>Fails&nbsp;the&nbsp;running&nbsp;test&nbsp;without&nbsp;any&nbsp;evaluation.&nbsp;This&nbsp;is&nbsp;commonly&nbsp;used&nbsp;when&nbsp;testing&nbsp;various<br>possible&nbsp;execution&nbsp;paths&nbsp;at&nbsp;once&nbsp;and&nbsp;you&nbsp;would&nbsp;like&nbsp;to&nbsp;signify&nbsp;a&nbsp;failure&nbsp;if&nbsp;an&nbsp;improper&nbsp;path<br>was&nbsp;taken.<br>Line&nbsp;6–10:&nbsp;This&nbsp;method&nbsp;tests&nbsp;the&nbsp;title&nbsp;attribute&nbsp;of&nbsp;the&nbsp;Sample&nbsp;class.&nbsp;The&nbsp;first&nbsp;test&nbsp;should<br>be&nbsp;of&nbsp;course&nbsp;that&nbsp;the&nbsp;attribute&nbsp;exists&nbsp;and&nbsp;has&nbsp;the&nbsp;expected&nbsp;initial&nbsp;value&nbsp;(line&nbsp;8).&nbsp;Then&nbsp;the&nbsp;title<br>attribute&nbsp;is&nbsp;changed&nbsp;and&nbsp;we&nbsp;check&nbsp;whether&nbsp;the&nbsp;value&nbsp;was&nbsp;really&nbsp;stored.&nbsp;This&nbsp;might&nbsp;seem&nbsp;like<br>overkill,&nbsp;but&nbsp;later&nbsp;you&nbsp;might&nbsp;change&nbsp;the&nbsp;title&nbsp;in&nbsp;a&nbsp;way&nbsp;that&nbsp;it&nbsp;uses&nbsp;properties&nbsp;instead.&nbsp;Then&nbsp;it<br>becomes&nbsp;very&nbsp;important&nbsp;to&nbsp;check&nbsp;whether&nbsp;this&nbsp;test&nbsp;still&nbsp;passes.<br>Line&nbsp;12–16:&nbsp;First&nbsp;we&nbsp;simply&nbsp;check&nbsp;that&nbsp;getDescription()&nbsp;returns&nbsp;the&nbsp;correct&nbsp;default&nbsp;value.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;want&nbsp;to&nbsp;use&nbsp;other&nbsp;API&nbsp;calls&nbsp;like&nbsp;setDescription()&nbsp;we&nbsp;set&nbsp;a&nbsp;new&nbsp;value&nbsp;of&nbsp;the<br>description&nbsp;via&nbsp;the&nbsp;implementation-internal&nbsp;description&nbsp;attribute&nbsp;(line&nbsp;15).&nbsp;This&nbsp;is&nbsp;okay!&nbsp;Unit<br>tests&nbsp;can&nbsp;make&nbsp;use&nbsp;of&nbsp;implementation-specific&nbsp;attributes&nbsp;and&nbsp;methods.&nbsp;Finally&nbsp;we&nbsp;just&nbsp;check&nbsp;that<br>the&nbsp;correct&nbsp;value&nbsp;is&nbsp;returned.<br><hr><A name=5></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>5<br>Line&nbsp;18–25:&nbsp;On&nbsp;line&nbsp;21–24&nbsp;it&nbsp;is&nbsp;checked&nbsp;that&nbsp;both&nbsp;regular&nbsp;and&nbsp;unicode&nbsp;strings&nbsp;are&nbsp;set&nbsp;correctly.<br>In&nbsp;the&nbsp;last&nbsp;line&nbsp;of&nbsp;the&nbsp;test&nbsp;we&nbsp;make&nbsp;sure&nbsp;that&nbsp;no&nbsp;other&nbsp;type&nbsp;of&nbsp;objects&nbsp;can&nbsp;be&nbsp;set&nbsp;as&nbsp;a&nbsp;description<br>and&nbsp;that&nbsp;an&nbsp;error&nbsp;is&nbsp;raised.<br>28–31:&nbsp;This&nbsp;method&nbsp;returns&nbsp;a&nbsp;test&nbsp;suite&nbsp;that&nbsp;includes&nbsp;all&nbsp;test&nbsp;cases&nbsp;created&nbsp;in&nbsp;this&nbsp;module.&nbsp;It&nbsp;is<br>used&nbsp;by&nbsp;the&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner&nbsp;when&nbsp;it&nbsp;picks&nbsp;up&nbsp;all&nbsp;available&nbsp;tests.&nbsp;You&nbsp;would&nbsp;basically&nbsp;add&nbsp;the<br>line&nbsp;unittest.makeSuite(TestCaseClass)&nbsp;for&nbsp;each&nbsp;additional&nbsp;test&nbsp;case.<br>33–34:&nbsp;In&nbsp;order&nbsp;to&nbsp;make&nbsp;the&nbsp;test&nbsp;module&nbsp;runnable&nbsp;by&nbsp;itself,&nbsp;you&nbsp;can&nbsp;execute&nbsp;unittest.main()<br>when&nbsp;the&nbsp;module&nbsp;is&nbsp;run.<br>44.3<br>Running&nbsp;the&nbsp;Tests<br>You&nbsp;can&nbsp;run&nbsp;the&nbsp;test&nbsp;by&nbsp;simply&nbsp;calling&nbsp;pythontest&nbsp;sample.py&nbsp;from&nbsp;the&nbsp;directory&nbsp;you&nbsp;saved&nbsp;the<br>file&nbsp;in.&nbsp;Here&nbsp;is&nbsp;the&nbsp;result&nbsp;you&nbsp;should&nbsp;see:<br>.<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.001s<br>The&nbsp;three&nbsp;dots&nbsp;represent&nbsp;the&nbsp;three&nbsp;tests&nbsp;that&nbsp;were&nbsp;run.&nbsp;If&nbsp;a&nbsp;test&nbsp;had&nbsp;failed,&nbsp;it&nbsp;would&nbsp;have&nbsp;been<br>reported&nbsp;pointing&nbsp;out&nbsp;the&nbsp;failing&nbsp;test&nbsp;and&nbsp;providing&nbsp;a&nbsp;small&nbsp;traceback.<br>When&nbsp;using&nbsp;the&nbsp;default&nbsp;Zope&nbsp;3&nbsp;test&nbsp;runner,&nbsp;tests&nbsp;will&nbsp;be&nbsp;picked&nbsp;up&nbsp;as&nbsp;long&nbsp;as&nbsp;they&nbsp;follow&nbsp;some<br>conventions.<br>•&nbsp;The&nbsp;tests&nbsp;must&nbsp;either&nbsp;be&nbsp;in&nbsp;a&nbsp;package&nbsp;or&nbsp;be&nbsp;a&nbsp;module&nbsp;called&nbsp;tests.<br>•&nbsp;If&nbsp;tests&nbsp;is&nbsp;a&nbsp;package,&nbsp;then&nbsp;all&nbsp;test&nbsp;modules&nbsp;inside&nbsp;must&nbsp;also&nbsp;have&nbsp;a&nbsp;name&nbsp;starting&nbsp;with&nbsp;test,<br>as&nbsp;it&nbsp;is&nbsp;the&nbsp;case&nbsp;with&nbsp;our&nbsp;name&nbsp;test&nbsp;sample.py.<br>•&nbsp;The&nbsp;test&nbsp;module&nbsp;must&nbsp;be&nbsp;somewhere&nbsp;in&nbsp;the&nbsp;Zope&nbsp;3&nbsp;source&nbsp;tree,&nbsp;since&nbsp;the&nbsp;test&nbsp;runner&nbsp;looks<br>only&nbsp;for&nbsp;files&nbsp;there.<br>In&nbsp;our&nbsp;case,&nbsp;you&nbsp;could&nbsp;simply&nbsp;create&nbsp;a&nbsp;tests&nbsp;package&nbsp;in&nbsp;ZOPE3/src&nbsp;(do&nbsp;not&nbsp;forget&nbsp;the<br>init&nbsp;.<br>py&nbsp;file).&nbsp;Then&nbsp;place&nbsp;the&nbsp;test&nbsp;sample.py&nbsp;file&nbsp;into&nbsp;this&nbsp;directory.<br>You&nbsp;you&nbsp;can&nbsp;use&nbsp;the&nbsp;test&nbsp;runner&nbsp;to&nbsp;run&nbsp;only&nbsp;the&nbsp;sample&nbsp;tests&nbsp;as&nbsp;follows&nbsp;from&nbsp;the&nbsp;Zope&nbsp;3&nbsp;root<br>directory:<br>python&nbsp;test.py&nbsp;-vp&nbsp;tests.test_sample<br>The&nbsp;-v&nbsp;option&nbsp;stands&nbsp;for&nbsp;verbose&nbsp;mode,&nbsp;so&nbsp;that&nbsp;detailed&nbsp;information&nbsp;about&nbsp;a&nbsp;test&nbsp;failure&nbsp;is<br>provided.&nbsp;The&nbsp;-p&nbsp;option&nbsp;enables&nbsp;a&nbsp;progress&nbsp;bar&nbsp;that&nbsp;tells&nbsp;you&nbsp;how&nbsp;many&nbsp;tests&nbsp;out&nbsp;of&nbsp;all&nbsp;have&nbsp;been<br>completed.&nbsp;There&nbsp;are&nbsp;many&nbsp;more&nbsp;options&nbsp;that&nbsp;can&nbsp;be&nbsp;specified.&nbsp;You&nbsp;can&nbsp;get&nbsp;a&nbsp;full&nbsp;list&nbsp;of&nbsp;them&nbsp;with<br>the&nbsp;option&nbsp;-h:&nbsp;pythontest.py-h.<br>The&nbsp;output&nbsp;of&nbsp;the&nbsp;call&nbsp;above&nbsp;is&nbsp;as&nbsp;follows:<br>nfiguration&nbsp;file&nbsp;found.<br>nning&nbsp;UNIT&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;UNIT&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>3/3&nbsp;(100.0%):&nbsp;test_title&nbsp;(tests.test_sample.SampleTest)<br>--------------------------------------------------------------------<br>n&nbsp;3&nbsp;tests&nbsp;in&nbsp;0.002s<br><hr><A name=6></a>6<br>CHAPTER&nbsp;44.&nbsp;WRITING&nbsp;BASIC&nbsp;UNIT&nbsp;TESTS<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;at&nbsp;level&nbsp;1<br>nning&nbsp;FUNCTIONAL&nbsp;tests&nbsp;from&nbsp;/opt/zope/Zope3<br>--------------------------------------------------------------------<br>n&nbsp;0&nbsp;tests&nbsp;in&nbsp;0.000s<br>Line&nbsp;1:&nbsp;The&nbsp;test&nbsp;runner&nbsp;uses&nbsp;a&nbsp;configuration&nbsp;file&nbsp;for&nbsp;some&nbsp;setup.&nbsp;This&nbsp;allows&nbsp;developers&nbsp;to&nbsp;use<br>the&nbsp;test&nbsp;runner&nbsp;for&nbsp;other&nbsp;projects&nbsp;as&nbsp;well.&nbsp;This&nbsp;message&nbsp;simply&nbsp;tells&nbsp;us&nbsp;that&nbsp;the&nbsp;configuration&nbsp;file<br>was&nbsp;found.<br>Line&nbsp;2–8:&nbsp;The&nbsp;unit&nbsp;tests&nbsp;are&nbsp;run.&nbsp;On&nbsp;line&nbsp;4&nbsp;you&nbsp;can&nbsp;see&nbsp;the&nbsp;progress&nbsp;bar.<br>Line&nbsp;9–15:&nbsp;The&nbsp;functional&nbsp;tests&nbsp;are&nbsp;run,&nbsp;since&nbsp;the&nbsp;default&nbsp;test&nbsp;runner&nbsp;runs&nbsp;both&nbsp;types&nbsp;of&nbsp;tests.<br>Since&nbsp;we&nbsp;do&nbsp;not&nbsp;have&nbsp;any&nbsp;functional&nbsp;tests&nbsp;in&nbsp;the&nbsp;specified&nbsp;module,&nbsp;there&nbsp;are&nbsp;no&nbsp;tests&nbsp;to&nbsp;run.&nbsp;To<br>just&nbsp;run&nbsp;the&nbsp;unit&nbsp;tests,&nbsp;use&nbsp;option&nbsp;-u&nbsp;and&nbsp;-f&nbsp;for&nbsp;just&nbsp;running&nbsp;the&nbsp;functional&nbsp;tests.&nbsp;See&nbsp;“Writing<br>Functional&nbsp;Tests”&nbsp;for&nbsp;more&nbsp;detials&nbsp;on&nbsp;functional&nbsp;tests.<br><hr><A name=7></a>44.3.&nbsp;RUNNING&nbsp;THE&nbsp;TESTS<br>7<br>Exercises<br>1.&nbsp;It&nbsp;is&nbsp;not&nbsp;very&nbsp;common&nbsp;to&nbsp;do&nbsp;the&nbsp;setup&nbsp;–&nbsp;in&nbsp;our&nbsp;case&nbsp;sample=Sample()&nbsp;–&nbsp;in&nbsp;every&nbsp;test<br>method.&nbsp;Instead&nbsp;there&nbsp;exists&nbsp;a&nbsp;method&nbsp;called&nbsp;setUp()&nbsp;and&nbsp;its&nbsp;counterpart&nbsp;tearDown&nbsp;that<br>are&nbsp;run&nbsp;before&nbsp;and&nbsp;after&nbsp;each&nbsp;test,&nbsp;respectively.&nbsp;Change&nbsp;the&nbsp;test&nbsp;code&nbsp;above,&nbsp;so&nbsp;that&nbsp;it&nbsp;uses<br>the&nbsp;setUp()&nbsp;method.&nbsp;In&nbsp;later&nbsp;chapters&nbsp;and&nbsp;the&nbsp;rest&nbsp;of&nbsp;the&nbsp;book&nbsp;we&nbsp;will&nbsp;frequently&nbsp;use&nbsp;this<br>method&nbsp;of&nbsp;setting&nbsp;up&nbsp;tests.<br>2.&nbsp;Currently&nbsp;the&nbsp;test&nbsp;setDescription()&nbsp;test&nbsp;only&nbsp;verifies&nbsp;that&nbsp;None&nbsp;is&nbsp;not&nbsp;allowed&nbsp;as&nbsp;input<br>value.<br>(a)&nbsp;Improve&nbsp;the&nbsp;test,&nbsp;so&nbsp;that&nbsp;all&nbsp;other&nbsp;builtin&nbsp;types&nbsp;are&nbsp;tested&nbsp;as&nbsp;well.<br>(b)&nbsp;Also,&nbsp;make&nbsp;sure&nbsp;that&nbsp;any&nbsp;objects&nbsp;inheriting&nbsp;from&nbsp;str&nbsp;or&nbsp;unicode&nbsp;pass&nbsp;as&nbsp;valid&nbsp;values.<br><hr>
Writing Basic Unit Tests<br> \ No newline at end of file
Difficulty<br>
Newcomer<br>
Skills<br>
• All you need to know is some Python.<br>
Problem/Task<br>
As you know by now, Zope 3 gains its incredible stability from testing any code in great detail. The<br>currently most common method is to write unit tests. This chapter introduces unit tests – which<br>are Zope 3 independent – and introduces some of the subtleties.<br>
Solution<br>
44.1<br>
Implementing the Sample Class<br>
Before we can write tests, we have to write some code that we can test. Here, we will implement<br>a simple class called Sample with a public attribute title and description that is accessed<br>via getDescription() and mutated using setDescription(). Further, the description must be<br>either a regular or unicode string.<br>
Since this code will not depend on Zope, open a file named test sample.py anywhere and add<br>
the following class:<br>
1 Sample(object):<br>
2<br>
&quot;&quot;&quot;A trivial Sample object.&quot;&quot;&quot;<br>
3<br>
4<br>
title = None<br>
5<br>
6<br>
def __init__(self):<br>
7<br>
&quot;&quot;&quot;Initialize object.&quot;&quot;&quot;<br>
8<br>
self._description = ’’<br>
9<br>
1<br>
<hr>
<A name=2></a>2<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
10<br>
def setDescription(self, value):<br>
11<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
12<br>
assert isinstance(value, (str, unicode))<br>
13<br>
self._description = value<br>
14<br>
15<br>
def getDescription(self):<br>
16<br>
&quot;&quot;&quot;Change the value of the description.&quot;&quot;&quot;<br>
17<br>
return self._description<br>
Line 4: The title is just publicly declared and a value of None is given. Therefore this is just<br>a regular attribute.<br>
Line 8: The actual description string will be stored in description.<br>
Line 12: Make sure that the description is only a regular or unicode string, like it was stated in<br>the requirements.<br>
If you wish you can now manually test the class with the interactive Python shell. Just start<br>
Python by entering python in your shell prompt. Note that you should be in the directory in<br>which test sample.py is located when starting Python (an alternative is of course to specify the<br>directory in your PYTHONPATH.)<br>
1 &gt;&gt;&gt; from test_sample import Sample<br>2 &gt;&gt;&gt; sample = Sample()<br>
3 &gt;&gt;&gt; print sample.title<br>4 None<br>
5 &gt;&gt;&gt; sample.title = ’Title’<br>
6 &gt;&gt;&gt; print sample.title<br>7 Title<br>
8 &gt;&gt;&gt; print sample.getDescription()<br>9<br>
10 &gt;&gt;&gt; sample.setDescription(’Hello World’)<br>
11 &gt;&gt;&gt; print sample.getDescription()<br>12 Hello World<br>
13 &gt;&gt;&gt; sample.setDescription(None)<br>
14 Traceback (most recent call last):<br>
15<br>
File &quot;&lt;stdin&gt;&quot;, line 1, in ?<br>
16<br>
File &quot;test_sample.py&quot;, line 31, in setDescription<br>
17<br>
assert isinstance(value, (str, unicode))<br>
18 AssertionError<br>
As you can see in the last test, non-string object types are not allowed as descriptions and an<br>
AssertionError is raised.<br>
44.2<br>
Writing the Unit Tests<br>
The goal of writing the unit tests is to convert this informal, manual, and interactive testing session<br>into a formal test class. Python provides already a module called unittest for this purpose, which<br>is a port of the Java-based unit testing product, JUnit, by Kent Beck and Erich Gamma. There are<br>three levels to the testing framework (this list deviates a bit from the original definitions as found<br>in the Python library documentation. 1).<br>
1 http://www.python.org/doc/current/lib/module-unittest.html<br>
<hr>
<A name=3></a>44.2. WRITING THE UNIT TESTS<br>
3<br>
The smallest unit is obviously the “test”, which is a single method in a TestCase class that<br>
tests the behavior of a small piece of code or a particular aspect of an implementation. The “test<br>case” is then a collection tests that share the same setup/inputs. On top of all of this sits the “test<br>suite” which is a collection of test cases and/or other test suites. Test suites combine tests that<br>should be executed together. With the correct setup (as shown in the example below), you can<br>then execute test suites. For large projects like Zope 3, it is useful to know that there is also the<br>concept of a test runner, which manages the test run of all or a set of tests. The runner provides<br>useful feedback to the application, so that various user interaces can be developed on top of it.<br>
But enough about the theory. In the following example, which you can simply put into the same<br>
file as your code above, you will see a test in common Zope 3 style.<br>
1 import unittest<br>2<br>
3 class SampleTest(unittest.TestCase):<br>4<br>
&quot;&quot;&quot;Test the Sample class&quot;&quot;&quot;<br>
5<br>
6<br>
def test_title(self):<br>
7<br>
sample = Sample()<br>
8<br>
self.assertEqual(sample.title, None)<br>
9<br>
sample.title = ’Sample Title’<br>
10<br>
self.assertEqual(sample.title, ’Sample Title’)<br>
11<br>
12<br>
def test_getDescription(self):<br>
13<br>
sample = Sample()<br>
14<br>
self.assertEqual(sample.getDescription(), ’’)<br>
15<br>
sample._description = &quot;Description&quot;<br>
16<br>
self.assertEqual(sample.getDescription(), ’Description’)<br>
17<br>
18<br>
def test_setDescription(self):<br>
19<br>
sample = Sample()<br>
20<br>
self.assertEqual(sample._description, ’’)<br>
21<br>
sample.setDescription(’Description’)<br>
22<br>
self.assertEqual(sample._description, ’Description’)<br>
23<br>
sample.setDescription(u’Description2’)<br>
24<br>
self.assertEqual(sample._description, u’Description2’)<br>
25<br>
self.assertRaises(AssertionError, sample.setDescription, None)<br>
26<br>
27<br>
28 def test_suite():<br>29<br>
return unittest.TestSuite((<br>
30<br>
unittest.makeSuite(SampleTest),<br>
31<br>
))<br>
32<br>
33 if __name__ == ’__main__’:<br>34<br>
unittest.main(defaultTest=’test_suite’)<br>
Line 3–4: We usually develop test classes which must inherit from TestCase. While often not<br>done, it is a good idea to give the class a meaningful docstring that describes the purpose of the<br>tests it includes.<br>
Line 6, 12 &amp; 18: When a test case is run, a method called runTests() is executed. While it<br>is possible to overrride this method to run tests differently, the default option will look for any<br>method whose name starts with test and execute it as a single test. This way we can create<br>a “test method” for each aspect, method, function or property of the code to be tested. This<br>default is very sensible and is used everywhere in Zope 3.<br>
<hr>
<A name=4></a>4<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
Note that there is no docstring for test methods. This is intentional. If a docstring is specified,<br>it is used instead of the method name to identify the test. When specifying a docstring, we have<br>noticed that it is very difficult to identify the test later; therefore the method name is a much<br>better choice.<br>
Line 8, 10, 14, . . . : The TestCase class implements a handful of methods that aid you with the<br>testing. Here are some of the most frequently used ones. For a complete list see the standard<br>Python documentation referenced above.<br>
• assertEqual(first,second[,msg])<br>
Checks whether the first and second value are equal. If the test fails, the msg or None<br>is returned.<br>
• assertNotEqual(first,second[,msg])<br>
This is simply the opposite to assertEqual() by checking for non-equality.<br>
• assertRaises(exception,callable,...)<br>
You expect the callable to raise exception when executed. After the callable you can<br>specify any amount of positional and keyword arguments for the callable. If you expect<br>a group of exceptions from the execution, you can make exception a tuple of possible<br>exceptions.<br>
• assert (expr[,msg])<br>
Assert checks whether the specified expression executes correctly. If not, the test fails and<br>msg or None is returned.<br>
• failUnlessEqual()<br>
This testing method is equivalent to assertEqual().<br>
• failUnless(expr[,msg])<br>
This method is equivalent to assert (expr[,msg]).<br>
• failif()<br>
This is the opposite to failUnless().<br>
• fail([msg])<br>
Fails the running test without any evaluation. This is commonly used when testing various<br>possible execution paths at once and you would like to signify a failure if an improper path<br>was taken.<br>
Line 6–10: This method tests the title attribute of the Sample class. The first test should<br>be of course that the attribute exists and has the expected initial value (line 8). Then the title<br>attribute is changed and we check whether the value was really stored. This might seem like<br>overkill, but later you might change the title in a way that it uses properties instead. Then it<br>becomes very important to check whether this test still passes.<br>
Line 12–16: First we simply check that getDescription() returns the correct default value.<br>Since we do not want to use other API calls like setDescription() we set a new value of the<br>description via the implementation-internal description attribute (line 15). This is okay! Unit<br>tests can make use of implementation-specific attributes and methods. Finally we just check that<br>the correct value is returned.<br>
<hr>
<A name=5></a>44.3. RUNNING THE TESTS<br>
5<br>
Line 18–25: On line 21–24 it is checked that both regular and unicode strings are set correctly.<br>In the last line of the test we make sure that no other type of objects can be set as a description<br>and that an error is raised.<br>
28–31: This method returns a test suite that includes all test cases created in this module. It is<br>used by the Zope 3 test runner when it picks up all available tests. You would basically add the<br>line unittest.makeSuite(TestCaseClass) for each additional test case.<br>
33–34: In order to make the test module runnable by itself, you can execute unittest.main()<br>when the module is run.<br>
44.3<br>
Running the Tests<br>
You can run the test by simply calling pythontest sample.py from the directory you saved the<br>file in. Here is the result you should see:<br>
.<br>--------------------------------------------------------------------<br>n 3 tests in 0.001s<br>
The three dots represent the three tests that were run. If a test had failed, it would have been<br>
reported pointing out the failing test and providing a small traceback.<br>
When using the default Zope 3 test runner, tests will be picked up as long as they follow some<br>
conventions.<br>
• The tests must either be in a package or be a module called tests.<br>
• If tests is a package, then all test modules inside must also have a name starting with test,<br>
as it is the case with our name test sample.py.<br>
• The test module must be somewhere in the Zope 3 source tree, since the test runner looks<br>
only for files there.<br>
In our case, you could simply create a tests package in ZOPE3/src (do not forget the<br>
init .<br>
py file). Then place the test sample.py file into this directory.<br>
You you can use the test runner to run only the sample tests as follows from the Zope 3 root<br>
directory:<br>
python test.py -vp tests.test_sample<br>
The -v option stands for verbose mode, so that detailed information about a test failure is<br>
provided. The -p option enables a progress bar that tells you how many tests out of all have been<br>completed. There are many more options that can be specified. You can get a full list of them with<br>the option -h: pythontest.py-h.<br>
The output of the call above is as follows:<br>
nfiguration file found.<br>nning UNIT tests at level 1<br>nning UNIT tests from /opt/zope/Zope3<br>
3/3 (100.0%): test_title (tests.test_sample.SampleTest)<br>
--------------------------------------------------------------------<br>n 3 tests in 0.002s<br>
<hr>
<A name=6></a>6<br>
CHAPTER 44. WRITING BASIC UNIT TESTS<br>
nning FUNCTIONAL tests at level 1<br>nning FUNCTIONAL tests from /opt/zope/Zope3<br>
--------------------------------------------------------------------<br>n 0 tests in 0.000s<br>
Line 1: The test runner uses a configuration file for some setup. This allows developers to use<br>the test runner for other projects as well. This message simply tells us that the configuration file<br>was found.<br>
Line 2–8: The unit tests are run. On line 4 you can see the progress bar.<br>
Line 9–15: The functional tests are run, since the default test runner runs both types of tests.<br>Since we do not have any functional tests in the specified module, there are no tests to run. To<br>just run the unit tests, use option -u and -f for just running the functional tests. See “Writing<br>Functional Tests” for more detials on functional tests.<br>
<hr>
<A name=7></a>44.3. RUNNING THE TESTS<br>
7<br>
Exercises<br>
1. It is not very common to do the setup – in our case sample=Sample() – in every test<br>
method. Instead there exists a method called setUp() and its counterpart tearDown that<br>are run before and after each test, respectively. Change the test code above, so that it uses<br>the setUp() method. In later chapters and the rest of the book we will frequently use this<br>method of setting up tests.<br>
2. Currently the test setDescription() test only verifies that None is not allowed as input<br>
value.<br>
(a) Improve the test, so that all other builtin types are tested as well.<br>
(b) Also, make sure that any objects inheriting from str or unicode pass as valid values.<br>
<hr>
\ No newline at end of file
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
<h2> Testing Markdown </h2> <h2> Testing Markdown </h2>
<p> <code>code</code> and <em>italic</em> and <em>bold</em> and even a <a href="http://plone.org">link</a>. <p> <code>code</code> and <em>italic</em> and <em>bold</em> and even a <a href="http://plone.org">link</a>.
</p> </p>
<p>Fööbär</p>
<h2 class="title">Heading 1</h2> <h2 class="title">Heading 1</h2>
<p>Some text.</p> <p>Some text.</p>
<div class="section"> <div class="section" id="heading-2">
<h3><a id="heading-2" name="heading-2">Heading 2</a></h3> <h3>Heading 2</h3>
<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p> <p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p>
</div> </div>
<h2 class="title">Title</h2> <h2 class="title">Title</h2>
<h3 class="subtitle">Subtitle</h3> <h3 class="subtitle">Subtitle</h3>
<p>This is a test document to make sure subtitle gets the right heading.</p> <p>This is a test document to make sure subtitle gets the right heading.</p>
<div class="section"> <div class="section" id="now-the-real-heading">
<h3><a id="now-the-real-heading" name="now-the-real-heading">Now the real heading</a></h3> <h3>Now the real heading</h3>
<p>The brown fox jumped over the lazy dog.</p> <p>The brown fox jumped over the lazy dog.</p>
<div class="section"> <div class="section" id="with-a-subheading">
<h4><a id="with-a-subheading" name="with-a-subheading">With a subheading</a></h4> <h4>With a subheading</h4>
<p>Some text, bla ble bli blo blu. Yes, i know this is <a class="reference" href="http://www.example.com">Stupid</a>.</p> <p>Some text, bla ble bli blo blu. Yes, i know this is<a class="reference external" href="http://www.example.com">Stupid</a>.</p>
</div> </div>
</div> </div>
...@@ -6,6 +6,10 @@ ...@@ -6,6 +6,10 @@
</tr> </tr>
</table> </table>
<p>This is a text used as a blind text.</p> <p>This is a text used as a blind text.</p>
<div><![CDATA[
Some CDATA text.
]]>
</div>
<ul> <ul>
<li>A sample list item1</li> <li>A sample list item1</li>
<li>A sample list item2</li> <li>A sample list item2</li>
......
...@@ -67,6 +67,15 @@ class DummyHtmlFilter2(BaseTransform): ...@@ -67,6 +67,15 @@ class DummyHtmlFilter2(BaseTransform):
data.setData("<div class='dummy'>%s</div>" % orig) data.setData("<div class='dummy'>%s</div>" % orig)
return data return data
class QuxToVHost(DummyHtmlFilter1):
__name__ = 'qux_to_vhost'
def convert(self, orig, data, context, **kwargs):
data.setData(re.sub('qux', context.REQUEST['SERVER_URL'], orig))
return data
class TransformNoIO(BaseTransform): class TransformNoIO(BaseTransform):
implements(ITransform) implements(ITransform)
...@@ -223,6 +232,52 @@ class TestEngine(ATSiteTestCase): ...@@ -223,6 +232,52 @@ class TestEngine(ATSiteTestCase):
out = self.engine.convertTo(mt, other_data, mimetype=mt, object=self) out = self.engine.convertTo(mt, other_data, mimetype=mt, object=self)
self.failUnlessEqual(out.getData(), other_data, out.getData()) self.failUnlessEqual(out.getData(), other_data, out.getData())
def testCacheWithVHost(self):
"""Ensure that the transform cache key includes virtual
hosting so that transforms which are dependent on the virtual
hosting don't get invalid data from the cache. This happens,
for example, in the resolve UID functionality used by visual
editors."""
mt = 'text/x-html-safe'
self.engine.registerTransform(QuxToVHost())
required = ['qux_to_vhost']
self.engine.manage_addPolicy(mt, required)
data = '<a href="qux">vhost link</a>'
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self.folder,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://nohost">vhost link</a>',
out.getData())
# Test when object is not a context
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://nohost">vhost link</a>',
out.getData())
# Change the virtual hosting
self.folder.REQUEST['SERVER_URL'] = 'http://otherhost'
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self.folder,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://otherhost">vhost link</a>',
out.getData())
# Test when object is not a context
out = self.engine.convertTo(
mt, data, mimetype='text/html', object=self,
context=self.folder)
self.failUnlessEqual(
out.getData(), '<a href="http://otherhost">vhost link</a>',
out.getData())
def test_suite(): def test_suite():
from unittest import TestSuite, makeSuite from unittest import TestSuite, makeSuite
......
...@@ -16,6 +16,87 @@ class TestGraph(ATSiteTestCase): ...@@ -16,6 +16,87 @@ class TestGraph(ATSiteTestCase):
out = self.engine.convertTo('text/plain', data, filename=FILE_PATH) out = self.engine.convertTo('text/plain', data, filename=FILE_PATH)
self.failUnless(out.getData()) self.failUnless(out.getData())
def testFindPath(self):
originalMap = self.engine._mtmap
"""
The dummy map used for this test corresponds to a graph
depicted in ASCII art below :
+---+
| |
| v
+-->1<-->2-->4-->6<--7
^ ^ |
| | |
v | |
3<---+ |
^ |
| |
v |
5<-------+
"""
# we need a DummyTransform class
class DT:
def __init__(self, name):
self._name = name
def name(self):
return self._name
dummyMap1 = {
'1': { '1': [DT('transform1-1')],
'2': [DT('transform1-2')],
'3': [DT('transform1-3')]},
'2': { '1': [DT('transform2-1')],
'3': [DT('transform2-3')],
'4': [DT('transform2-4')]},
'3': { '1': [DT('transform3-1')],
'2': [DT('transform3-2')],
'5': [DT('transform3-5')]},
'4': { '5': [DT('transform4-5')],
'6': [DT('transform4-6')]},
'5': { '3': [DT('transform5-3')]},
'7': { '6': [DT('transform7-6')]}
}
expectedPathes = {
'1-1': [],
'1-2': ['transform1-2'],
'1-3': ['transform1-3'],
'1-4': ['transform1-2', 'transform2-4'],
'1-5': ['transform1-3', 'transform3-5'],
'1-6': ['transform1-2', 'transform2-4', 'transform4-6'],
'1-7': None,
'2-1': ['transform2-1'],
'2-2': [],
'2-4': ['transform2-4'],
'4-2': ['transform4-5', 'transform5-3', 'transform3-2'],
'5-3': ['transform5-3']
}
self.engine._mtmap = dummyMap1
for orig in ['1','2','3','4','5','6','7']:
for target in ['1','2','3','4','5','6','7']:
# build the name of the path
pathName = orig + '-' + target
# do we have any expectation for this path ?
if pathName in expectedPathes.keys():
# we do. Here is the expected shortest path
expectedPath = expectedPathes[pathName]
# what's the shortest path according to the engine ?
gotPath = self.engine._findPath(orig,target)
# just keep the name of the transforms, please
if gotPath is not None:
gotPath = [transform.name() for transform in gotPath]
# this must be the same as in our expectation
self.assertEquals(expectedPath, gotPath)
self.engine._mtmap = originalMap
def testFindPathWithEmptyTransform(self):
""" _findPath should not throw "index out of range" when dealing with
empty transforms list
"""
dummyMap = {'1': {'2': []}}
self.engine._mtmap = dummyMap
self.engine._findPath('1','2')
def testIdentity(self): def testIdentity(self):
orig = 'Some text' orig = 'Some text'
converted = self.engine.convertTo( converted = self.engine.convertTo(
......
import os import os
import logging import logging
from Testing import ZopeTestCase
from Products.Archetypes.tests.atsitetestcase import ATSiteTestCase from Products.Archetypes.tests.atsitetestcase import ATSiteTestCase
from Products.CMFCore.utils import getToolByName
from utils import input_file_path, output_file_path, normalize_html,\ from utils import input_file_path, output_file_path, normalize_html,\
load, matching_inputs load, matching_inputs
from Products.PortalTransforms.data import datastream from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.interfaces import IDataStream from Products.PortalTransforms.interfaces import IDataStream
from Products.PortalTransforms.interfaces import idatastream
from Products.MimetypesRegistry.MimeTypesTool import MimeTypesTool
from Products.PortalTransforms.TransformEngine import TransformTool
from Products.PortalTransforms.libtransforms.utils import MissingBinary from Products.PortalTransforms.libtransforms.utils import MissingBinary
from Products.PortalTransforms.transforms.image_to_gif import image_to_gif from Products.PortalTransforms.transforms.image_to_gif import image_to_gif
...@@ -24,7 +21,6 @@ from Products.PortalTransforms.transforms.textile_to_html import HAS_TEXTILE ...@@ -24,7 +21,6 @@ from Products.PortalTransforms.transforms.textile_to_html import HAS_TEXTILE
from Products.PortalTransforms.transforms.markdown_to_html import HAS_MARKDOWN from Products.PortalTransforms.transforms.markdown_to_html import HAS_MARKDOWN
from os.path import exists from os.path import exists
import sys
# we have to set locale because lynx output is locale sensitive ! # we have to set locale because lynx output is locale sensitive !
os.environ['LC_ALL'] = 'C' os.environ['LC_ALL'] = 'C'
logger = logging.getLogger('PortalTransforms') logger = logging.getLogger('PortalTransforms')
...@@ -59,9 +55,11 @@ class TransformTest(ATSiteTestCase): ...@@ -59,9 +55,11 @@ class TransformTest(ATSiteTestCase):
got = self.normalize(got) got = self.normalize(got)
output.close() output.close()
self.assertEquals(got, expected, got_start = got.strip()[:30]
expected_start = expected.strip()[:30]
self.assertEquals(got_start, expected_start,
'[%s]\n\n!=\n\n[%s]\n\nIN %s(%s)' % ( '[%s]\n\n!=\n\n[%s]\n\nIN %s(%s)' % (
got, expected, self.transform.name(), self.input)) got_start, expected_start, self.transform.name(), self.input))
self.assertEquals(self.subobjects, len(res_data.getSubObjects()), self.assertEquals(self.subobjects, len(res_data.getSubObjects()),
'%s\n\n!=\n\n%s\n\nIN %s(%s)' % ( '%s\n\n!=\n\n%s\n\nIN %s(%s)' % (
self.subobjects, len(res_data.getSubObjects()), self.subobjects, len(res_data.getSubObjects()),
...@@ -70,13 +68,13 @@ class TransformTest(ATSiteTestCase): ...@@ -70,13 +68,13 @@ class TransformTest(ATSiteTestCase):
def testSame(self): def testSame(self):
try: try:
self.do_convert(filename=self.input) self.do_convert(filename=self.input)
except MissingBinary, e: except MissingBinary:
pass pass
def testSameNoFilename(self): def testSameNoFilename(self):
try: try:
self.do_convert() self.do_convert()
except MissingBinary, e: except MissingBinary:
pass pass
def __repr__(self): def __repr__(self):
...@@ -86,12 +84,13 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -86,12 +84,13 @@ class PILTransformsTest(ATSiteTestCase):
def afterSetUp(self): def afterSetUp(self):
ATSiteTestCase.afterSetUp(self) ATSiteTestCase.afterSetUp(self)
self.pt = self.portal.portal_transforms self.pt = self.portal.portal_transforms
self.mimetypes_registry = getToolByName(self.portal, 'mimetypes_registry')
def test_image_to_bmp(self): def test_image_to_bmp(self):
self.pt.registerTransform(image_to_bmp()) self.pt.registerTransform(image_to_bmp())
imgFile = open(input_file_path('logo.jpg'), 'rb') imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/x-ms-bmp',orig=data) data = self.pt.convertTo(target_mimetype='image/x-ms-bmp',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-ms-bmp') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-ms-bmp')
...@@ -99,7 +98,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -99,7 +98,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_gif()) self.pt.registerTransform(image_to_gif())
imgFile = open(input_file_path('logo.png'), 'rb') imgFile = open(input_file_path('logo.png'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png')
data = self.pt.convertTo(target_mimetype='image/gif',orig=data) data = self.pt.convertTo(target_mimetype='image/gif',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/gif') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/gif')
...@@ -107,7 +106,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -107,7 +106,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_jpeg()) self.pt.registerTransform(image_to_jpeg())
imgFile = open(input_file_path('logo.gif'), 'rb') imgFile = open(input_file_path('logo.gif'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif')
data = self.pt.convertTo(target_mimetype='image/jpeg',orig=data) data = self.pt.convertTo(target_mimetype='image/jpeg',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/jpeg') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/jpeg')
...@@ -115,7 +114,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -115,7 +114,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_png()) self.pt.registerTransform(image_to_png())
imgFile = open(input_file_path('logo.jpg'), 'rb') imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/png',orig=data) data = self.pt.convertTo(target_mimetype='image/png',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/png') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/png')
...@@ -123,7 +122,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -123,7 +122,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_pcx()) self.pt.registerTransform(image_to_pcx())
imgFile = open(input_file_path('logo.gif'), 'rb') imgFile = open(input_file_path('logo.gif'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/gif') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/gif')
data = self.pt.convertTo(target_mimetype='image/pcx',orig=data) data = self.pt.convertTo(target_mimetype='image/pcx',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/pcx') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/pcx')
...@@ -131,7 +130,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -131,7 +130,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_ppm()) self.pt.registerTransform(image_to_ppm())
imgFile = open(input_file_path('logo.png'), 'rb') imgFile = open(input_file_path('logo.png'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/png') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/png')
data = self.pt.convertTo(target_mimetype='image/x-portable-pixmap',orig=data) data = self.pt.convertTo(target_mimetype='image/x-portable-pixmap',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-portable-pixmap') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/x-portable-pixmap')
...@@ -139,7 +138,7 @@ class PILTransformsTest(ATSiteTestCase): ...@@ -139,7 +138,7 @@ class PILTransformsTest(ATSiteTestCase):
self.pt.registerTransform(image_to_tiff()) self.pt.registerTransform(image_to_tiff())
imgFile = open(input_file_path('logo.jpg'), 'rb') imgFile = open(input_file_path('logo.jpg'), 'rb')
data = imgFile.read() data = imgFile.read()
self.failUnlessEqual(self.portal.mimetypes_registry.classify(data),'image/jpeg') self.failUnlessEqual(self.mimetypes_registry.classify(data),'image/jpeg')
data = self.pt.convertTo(target_mimetype='image/tiff',orig=data) data = self.pt.convertTo(target_mimetype='image/tiff',orig=data)
self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/tiff') self.failUnlessEqual(data.getMetadata()['mimetype'], 'image/tiff')
......
...@@ -5,6 +5,7 @@ from sys import modules ...@@ -5,6 +5,7 @@ from sys import modules
from os.path import join, abspath, dirname, basename from os.path import join, abspath, dirname, basename
def normalize_html(s): def normalize_html(s):
s = re.sub(r"&nbsp;", " ", s)
s = re.sub(r"\s+", " ", s) s = re.sub(r"\s+", " ", s)
s = re.sub(r"(?s)\s+<", "<", s) s = re.sub(r"(?s)\s+<", "<", s)
s = re.sub(r"(?s)>\s+", ">", s) s = re.sub(r"(?s)>\s+", ">", s)
......
""" """
Uses the http://www.freewisdom.org/projects/python-markdown/ module to do its handy work Uses the http://www.freewisdom.org/projects/python-markdown/ module
author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006
Author: Tom Lazar <tom@tomster.org> at the archipelago sprint 2006
""" """
import os
from zope.interface import implements from zope.interface import implements
from Products.CMFDefault.utils import bodyfinder
from Products.PortalTransforms.interfaces import ITransform from Products.PortalTransforms.interfaces import ITransform
from Products.PortalTransforms.libtransforms.commandtransform import commandtransform
from Products.PortalTransforms.libtransforms.utils import bin_search
from Products.PortalTransforms.libtransforms.utils import sansext
from Products.PortalTransforms.utils import log from Products.PortalTransforms.utils import log
try: try:
...@@ -37,11 +30,16 @@ class markdown: ...@@ -37,11 +30,16 @@ class markdown:
def convert(self, orig, data, **kwargs): def convert(self, orig, data, **kwargs):
if HAS_MARKDOWN: if HAS_MARKDOWN:
html = markdown_transformer.markdown(orig) # markdown expects unicode input:
orig = unicode(orig.decode('utf-8'))
# PortalTransforms, however expects a string as result,
# so we encode the unicode result back to UTF8:
html = markdown_transformer.markdown(orig).encode('utf-8')
else: else:
html = orig html = orig
data.setData(html) data.setData(html)
return data return data
def register(): def register():
return markdown() return markdown()
import re, tempfile import os
import os, os.path from Products.PortalTransforms.libtransforms.utils import bodyfinder, scrubHTML
from Products.PortalTransforms.libtransforms.utils import bin_search, \
sansext, bodyfinder, scrubHTML
from Products.PortalTransforms.libtransforms.commandtransform import commandtransform from Products.PortalTransforms.libtransforms.commandtransform import commandtransform
class document(commandtransform): class document(commandtransform):
......
...@@ -31,7 +31,7 @@ VALID_TAGS['ins'] = 1 ...@@ -31,7 +31,7 @@ VALID_TAGS['ins'] = 1
VALID_TAGS['del'] = 1 VALID_TAGS['del'] = 1
VALID_TAGS['q'] = 1 VALID_TAGS['q'] = 1
VALID_TAGS['map'] = 1 VALID_TAGS['map'] = 1
VALID_TAGS['area'] = 1 VALID_TAGS['area'] = 0
VALID_TAGS['abbr'] = 1 VALID_TAGS['abbr'] = 1
VALID_TAGS['acronym'] = 1 VALID_TAGS['acronym'] = 1
VALID_TAGS['var'] = 1 VALID_TAGS['var'] = 1
...@@ -71,6 +71,10 @@ VALID_TAGS['source'] = 1 ...@@ -71,6 +71,10 @@ VALID_TAGS['source'] = 1
VALID_TAGS['time'] = 1 VALID_TAGS['time'] = 1
VALID_TAGS['video'] = 1 VALID_TAGS['video'] = 1
# add some tags to nasty. These should also probably be backported to CMFDefault.
NASTY_TAGS['style'] = 1 # this helps improve Word HTML cleanup.
NASTY_TAGS['meta'] = 1 # allowed by parsers, but can cause unexpected behavior
msg_pat = """ msg_pat = """
<div class="system-message"> <div class="system-message">
...@@ -203,7 +207,7 @@ class StrippingParser(HTMLParser): ...@@ -203,7 +207,7 @@ class StrippingParser(HTMLParser):
if not self.raise_error: continue if not self.raise_error: continue
else: raise IllegalHTML, 'Script event "%s" not allowed.' % k else: raise IllegalHTML, 'Script event "%s" not allowed.' % k
elif v is None: elif v is None:
self.result.append(' %s' % (k,)) self.result.append(' %s' % k)
elif remove_script and hasScript(v): elif remove_script and hasScript(v):
if not self.raise_error: continue if not self.raise_error: continue
else: raise IllegalHTML, 'Script URI "%s" not allowed.' % v else: raise IllegalHTML, 'Script URI "%s" not allowed.' % v
...@@ -238,6 +242,26 @@ class StrippingParser(HTMLParser): ...@@ -238,6 +242,26 @@ class StrippingParser(HTMLParser):
self.result.append('</%s>' % tag) self.result.append('</%s>' % tag)
#remTag = '</%s>' % tag #remTag = '</%s>' % tag
def parse_declaration(self, i):
"""Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
"""
j = None
if self.rawdata[i:i+9] == '<![CDATA[':
k = self.rawdata.find(']]>', i)
if k == -1:
k = len(self.rawdata)
data = self.rawdata[i+9:k]
j = k+3
self.result.append("<![CDATA[%s]]>" % data)
else:
try:
j = HTMLParser.parse_declaration(self, i)
except HTMLParseError:
toHandle = self.rawdata[i:]
self.result.append(toHandle)
j = i + len(toHandle)
return j
def getResult(self): def getResult(self):
return ''.join(self.result) return ''.join(self.result)
...@@ -291,6 +315,10 @@ class SafeHTML: ...@@ -291,6 +315,10 @@ class SafeHTML:
'output': self.output, 'output': self.output,
'valid_tags': VALID_TAGS, 'valid_tags': VALID_TAGS,
'nasty_tags': NASTY_TAGS, 'nasty_tags': NASTY_TAGS,
'stripped_attributes': ['lang','valign','halign','border','frame','rules','cellspacing','cellpadding','bgcolor'],
'stripped_combinations': {'table th td': 'width height'},
'style_whitelist': ['text-align', 'list-style-type', 'float'],
'class_blacklist': [],
'remove_javascript': 1, 'remove_javascript': 1,
'disable_transform': 0, 'disable_transform': 0,
'default_encoding': 'utf-8', 'default_encoding': 'utf-8',
...@@ -310,6 +338,19 @@ class SafeHTML: ...@@ -310,6 +338,19 @@ class SafeHTML:
'everything they contain (like applet, object). ' + 'everything they contain (like applet, object). ' +
'They are only deleted if they are not marked as valid_tags.', 'They are only deleted if they are not marked as valid_tags.',
('tag', 'value')), ('tag', 'value')),
'stripped_attributes': ('list',
'stripped_attributes',
'These attributes are stripped from any tag.'),
'stripped_combinations' : ('dict',
'stripped_combinations',
'These attributes are stripped from any tag.',
('tag', 'value')),
'style_whitelist': ('list',
'style_whitelist',
'These CSS styles are allowed in style attributes.'),
'class_blacklist': ('list',
'class_blacklist',
'These class names are not allowed in class attributes.'),
'remove_javascript' : ("int", 'remove_javascript' : ("int",
'remove_javascript', 'remove_javascript',
'1 to remove javascript attributes that begin with on (e.g. onClick) ' + '1 to remove javascript attributes that begin with on (e.g. onClick) ' +
...@@ -355,7 +396,9 @@ class SafeHTML: ...@@ -355,7 +396,9 @@ class SafeHTML:
repaired = 0 repaired = 0
while True: while True:
try: try:
orig = scrubHTML( # Do 2 passes. This provides more reliable filtering of certain
# malicious HTML (cf upstream commit svn10522).
for repeat in range(2): orig = scrubHTML(
orig, orig,
valid=self.config.get('valid_tags', {}), valid=self.config.get('valid_tags', {}),
nasty=self.config.get('nasty_tags', {}), nasty=self.config.get('nasty_tags', {}),
...@@ -366,6 +409,8 @@ class SafeHTML: ...@@ -366,6 +409,8 @@ class SafeHTML:
data.setData(msg_pat % ("Error", str(inst))) data.setData(msg_pat % ("Error", str(inst)))
break break
except HTMLParseError: except HTMLParseError:
if repeat:
raise # try to repair only on first pass
# ouch ! # ouch !
# HTMLParser is not able to parse very dirty HTML string # HTMLParser is not able to parse very dirty HTML string
if not repaired: if not repaired:
......
...@@ -45,7 +45,8 @@ class word_to_html: ...@@ -45,7 +45,8 @@ class word_to_html:
def convert(self, data, cache, **kwargs): def convert(self, data, cache, **kwargs):
orig_file = 'unknown.doc' orig_file = 'unknown.doc'
doc = None
try:
doc = document(orig_file, data) doc = document(orig_file, data)
doc.convert() doc.convert()
html = doc.html() html = doc.html()
...@@ -54,11 +55,13 @@ class word_to_html: ...@@ -54,11 +55,13 @@ class word_to_html:
objects = {} objects = {}
if images: if images:
doc.fixImages(path, images, objects) doc.fixImages(path, images, objects)
doc.cleanDir(doc.tmpdir)
cache.setData(html) cache.setData(html)
cache.setSubObjects(objects) cache.setSubObjects(objects)
return cache return cache
finally:
if doc is not None:
doc.cleanDir(doc.tmpdir)
def register(): def register():
return word_to_html() return word_to_html()
...@@ -6,37 +6,37 @@ from Products.PortalTransforms.libtransforms.utils import bin_search, MissingBin ...@@ -6,37 +6,37 @@ from Products.PortalTransforms.libtransforms.utils import bin_search, MissingBin
COMMAND_CONFIGS = ( COMMAND_CONFIGS = (
('lynx_dump', '.html', ('lynx_dump', '.html',
{'binary_path' : 'lynx', {'binary_path' : 'lynx',
'command_line' : '-dump %s', 'command_line' : '-dump %(input)s',
'inputs' : ('text/html',), 'inputs' : ('text/html',),
'output' : 'text/plain', 'output' : 'text/plain',
}), }),
('tidy_html', '.html', ('tidy_html', '.html',
{'binary_path' : 'tidy', {'binary_path' : 'tidy',
'command_line' : '%s', 'command_line' : '%(input)s',
'inputs' : ('text/html',), 'inputs' : ('text/html',),
'output' : 'text/html', 'output' : 'text/html',
}), }),
('rtf_to_html', None, ('rtf_to_html', None,
{'binary_path' : 'unrtf', {'binary_path' : 'unrtf',
'command_line' : '%s', 'command_line' : '%(input)s',
'inputs' : ('application/rtf',), 'inputs' : ('application/rtf',),
'output' : 'text/html', 'output' : 'text/html',
}), }),
('ppt_to_html', None, ('ppt_to_html', None,
{'binary_path' : 'ppthtml', {'binary_path' : 'ppthtml',
'command_line' : '%s', 'command_line' : '%(input)s',
'inputs' : ('application/vnd.ms-powerpoint',), 'inputs' : ('application/vnd.ms-powerpoint',),
'output' : 'text/html', 'output' : 'text/html',
}), }),
('excel_to_html', None, ('excel_to_html', None,
{'binary_path' : 'xlhtml', {'binary_path' : 'xlhtml',
'command_line' : '-nh -a %s', 'command_line' : '-nh -a %(input)s',
'inputs' : ('application/vnd.ms-excel',), 'inputs' : ('application/vnd.ms-excel',),
'output' : 'text/html', 'output' : 'text/html',
}), }),
('ps_to_text', None, ('ps_to_text', None,
{'binary_path' : 'ps2ascii', {'binary_path' : 'ps2ascii',
'command_line' : '%s', 'command_line' : '%(input)s',
'inputs' : ('application/postscript',), 'inputs' : ('application/postscript',),
'output' : 'text/plain', 'output' : 'text/plain',
}), }),
......
...@@ -8,10 +8,10 @@ class TransformException(Exception): ...@@ -8,10 +8,10 @@ class TransformException(Exception):
FB_REGISTRY = None FB_REGISTRY = None
# logging function # logging function
from zLOG import LOG, INFO from zLOG import LOG, DEBUG
#logger = logging.getLogger('PortalTransforms') #logger = logging.getLogger('PortalTransforms')
def log(message, severity=INFO): def log(message, severity=DEBUG):
LOG('PortalTransforms', severity, message) LOG('PortalTransforms', severity, message)
#logger.log(severity, message) #logger.log(severity, message)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment