source: products/quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/tests/testQSEOptimizer.py @ 1269

Last change on this file since 1269 was 1269, checked in by liebster, 15 years ago

Added tests manage custom meta tags.

  • Property svn:eol-style set to native
File size: 22.5 KB
Line 
1import re
2import string
3import urllib
4from zope.component import getMultiAdapter
5from Products.Five import zcml, fiveconfigure
6from Testing.ZopeTestCase import installPackage, hasPackage
7from Products.PloneTestCase import PloneTestCase
8from Products.CMFCore.utils import getToolByName
9from Products.CMFCore.permissions import ManagePortal
10from Products.CMFQuickInstallerTool.InstalledProduct import InstalledProduct
11from AccessControl.SecurityManagement import newSecurityManager, noSecurityManager
12
13import quintagroup.seoptimizer
14from quintagroup.seoptimizer.config import *
15
16zcml.load_site()
17zcml.load_config('overrides.zcml', quintagroup.seoptimizer)
18zcml.load_config('configure.zcml', quintagroup.seoptimizer)
19PRODUCT = 'quintagroup.seoptimizer'
20installPackage(PRODUCT)
21props = {'stop_words':STOP_WORDS, 'fields':FIELDS, 'additional_keywords': []}
22
23custom_metatags = [{'meta_name'    : 'metatag1',
24                    'meta_content' : 'metatag1value'},
25                   {'meta_name'    : 'metatag2',
26                    'meta_content' : 'metatag2value'},
27                   {'meta_name'    : 'metatag3',
28                    'meta_content' : ''}
29                  ]
30
31global_custom_metatags = {'default_custom_metatags':'metatag1|global_metatag1value\nmetatag4|global_metatag4value'}
32
33configlets = ({'id':'qSEOptimizer',
34    'name':'Search Engine Optimizer',
35    'action':'string:${portal_url}/seo-controlpanel',
36    'condition':'',
37    'category':'Products',
38    'visible':1,
39    'appId':'qSEOptimizer',
40    'permission':ManagePortal},)
41
42qSEO_CONTENT = ['File','Document','News Item']
43qSEO_FOLDER  = []
44qSEO_TYPES   = qSEO_CONTENT + qSEO_FOLDER
45
46
47PloneTestCase.setupPloneSite()
48
49class TestBeforeInstall(PloneTestCase.FunctionalTestCase):
50
51    def afterSetUp(self):
52        self.basic_auth = 'mgr:mgrpw'
53        self.portal_path = '/%s' % self.portal.absolute_url(1)
54
55    def testAccessPortalRootAnonymous(self):
56        response = self.publish(self.portal_path)
57        self.assertEqual(response.getStatus(), 200)
58
59    def testAccessPortalRootAuthenticated(self):
60        response = self.publish(self.portal_path, self.basic_auth)
61        self.assertEqual(response.getStatus(), 200)
62
63
64class TestInstallation(PloneTestCase.PloneTestCase):
65
66    def afterSetUp(self):
67        self.properties = getToolByName(self.portal, 'portal_properties')
68        self.qi = self.portal.portal_quickinstaller
69        self.qi.installProduct(PRODUCT)
70
71    def testAddingPropertySheet(self):
72        """ Test adding property sheet to portal_properties tool """
73        self.failUnless(hasattr(self.properties.aq_base, PROPERTY_SHEET))
74
75    def testAddingPropertyFields(self):
76        """ Test adding property field to portal_properties.maps_properties sheet """
77        map_sheet = self.properties[PROPERTY_SHEET]
78        for key, value in props.items():
79            self.failUnless(map_sheet.hasProperty(key) and list(map_sheet.getProperty(key)) == value)
80
81    def test_configlet_install(self):
82        configTool = getToolByName(self.portal, 'portal_controlpanel', None)
83        self.assert_(PRODUCT in [a.getId() for a in configTool.listActions()], 'Configlet not found')
84
85    def test_actions_install(self):
86        portal_types = getToolByName(self.portal, 'portal_types')
87        for ptype in portal_types.objectValues():
88            try:
89                #for Plone-2.5 and higher
90                acts = filter(lambda x: x.id == 'seo_properties', ptype.listActions())
91                action = acts and acts[0] or None
92            except AttributeError:
93                action = ptype.getActionById('seo_properties', default=None )
94
95            if ptype.getId() in qSEO_TYPES:
96                self.assert_(action, 'Action for %s not found' % ptype.getId())
97            else:
98                self.assert_(not action, 'Action found for %s' % ptype.getId())
99
100    def test_skins_install(self):
101        skinstool=getToolByName(self.portal, 'portal_skins')
102
103        for skin in skinstool.getSkinSelections():
104            path = skinstool.getSkinPath(skin)
105            path = map( string.strip, string.split( path,',' ) )
106            self.assert_(PRODUCT in path, 'qSEOptimizer layer not found in %s' %skin)
107
108    def test_versionedskin_install(self):
109        skinstool=getToolByName(self.portal, 'portal_skins')
110        mtool = getToolByName(self.portal, 'portal_migration')
111        plone_version = mtool.getFileSystemVersion()
112        if plone_version < "3":
113            for skin in skinstool.getSkinSelections():
114                path = skinstool.getSkinPath(skin)
115                path = map( string.strip, string.split( path,',' ) )
116                self.assert_(PRODUCT+'/%s' % plone_version in path, 'qSEOptimizer versioned layer not found in %s' %skin)
117
118    def test_actions_uninstall(self):
119        self.qi.uninstallProducts([PRODUCT])
120        self.assertNotEqual(self.qi.isProductInstalled(PRODUCT), True,'qSEOptimizer is already installed')
121        portal_types = getToolByName(self.portal, 'portal_types')
122        for ptype in portal_types.objectValues():
123            try:
124                #for Plone-2.5 and higher
125                acts = filter(lambda x: x.id == 'seo_properties', ptype.listActions())
126                action = acts and acts[0] or None
127            except AttributeError:
128                action = ptype.getActionById('seo_properties', default=None )
129
130            self.assert_(not action, 'Action for %s found after uninstallation' % ptype.getId())
131
132    def test_skins_uninstall(self):
133        self.qi.uninstallProducts([PRODUCT])
134        self.assertNotEqual(self.qi.isProductInstalled(PRODUCT), True,'qSEOptimizer is already installed')
135        skinstool=getToolByName(self.portal, 'portal_skins')
136
137        for skin in skinstool.getSkinSelections():
138            path = skinstool.getSkinPath(skin)
139            path = map( string.strip, string.split( path,',' ) )
140            self.assert_(not PRODUCT in path, 'qSEOptimizer layer found in %s after uninstallation' %skin)
141
142    def test_versionedskin_uninstall(self):
143        self.qi.uninstallProducts([PRODUCT])
144        self.assertNotEqual(self.qi.isProductInstalled(PRODUCT), True,'qSEOptimizer is already installed')
145        skinstool=getToolByName(self.portal, 'portal_skins')
146        mtool = getToolByName(self.portal, 'portal_migration')
147        plone_version = mtool.getFileSystemVersion()
148
149        for skin in skinstool.getSkinSelections():
150            path = skinstool.getSkinPath(skin)
151            path = map( string.strip, string.split( path,',' ) )
152            self.assert_(not PRODUCT+'/%s' % plone_version in path, 'qSEOptimizer versioned layer found in %s after uninstallation' %skin)
153
154    def test_configlet_uninstall(self):
155        self.qi.uninstallProducts([PRODUCT])
156        self.assertNotEqual(self.qi.isProductInstalled(PRODUCT), True,'qSEOptimizer is already installed')
157
158        configTool = getToolByName(self.portal, 'portal_controlpanel', None)
159        self.assert_(not PRODUCT in [a.getId() for a in configTool.listActions()], 'Configlet found after uninstallation')
160
161
162class TestResponse(PloneTestCase.FunctionalTestCase):
163
164    def afterSetUp(self):
165        self.qi = self.portal.portal_quickinstaller
166        self.qi.installProduct(PRODUCT)
167        #self.portal.changeSkin('Plone Default')
168
169        self.basic_auth = 'mgr:mgrpw'
170        self.loginAsPortalOwner()
171
172        '''Preparation for functional testing'''
173        my_doc = self.portal.invokeFactory('Document', id='my_doc')
174        my_doc = self.portal['my_doc']
175        self.canonurl = 'http://test.site.com/test.html'
176        self.sp = self.portal.portal_properties.seo_properties
177        self.sp.manage_changeProperties(**global_custom_metatags)
178
179        my_doc.qseo_properties_edit(title='hello world', title_override=1,
180                                    description='it is description', description_override=1,
181                                    keywords='my1|key2', keywords_override=1,
182                                    html_comment='no comments', html_comment_override=1,
183                                    robots='ALL', robots_override=1,
184                                    distribution='Global', distribution_override=1,
185                                    canonical=self.canonurl, canonical_override=1,
186                                    custommetatags=custom_metatags, custommetatags_override=1)
187
188        wf_tool = self.portal.portal_workflow
189        wf_tool.doActionFor(my_doc, 'publish')
190
191        abs_path = "/%s" % my_doc.absolute_url(1)
192        self.abs_path = abs_path
193        self.my_doc = my_doc
194        self.html = self.publish(abs_path, self.basic_auth).getBody()
195
196        # now setup page with title equal to plone site's title
197        my_doc2 = self.portal.invokeFactory('Document', id='my_doc2')
198        my_doc2 = self.portal['my_doc2']
199        my_doc2.update(title=self.portal.Title())
200        wf_tool.doActionFor(my_doc2, 'publish')
201        abs_path2 = "/%s" % my_doc2.absolute_url(1)
202        self.html2 = self.publish(abs_path2, self.basic_auth).getBody()
203
204    def testTitle(self):
205        m = re.match('.*<title>\\s*hello world\\s*</title>', self.html, re.S|re.M)
206        self.assert_(m, 'Title not set in')
207
208    def testTitleDuplication(self):
209        """If we are not overriding page title and current page title equals title of the plone site
210        then there should be no concatenation of both titles. Only one should be displayed.
211        """
212        m = re.match('.*<title>\\s*%s\\s*</title>' % self.portal.Title(), self.html2, re.S|re.M)
213        self.assert_(m, 'Title is not set correctly, perhaps it is duplicated with plone site title')
214
215    def testDescription(self):
216        m = re.match('.*<meta name="description" content="it is description" />', self.html, re.S|re.M)
217        self.assert_(m, 'Description not set in')
218
219    def testKeywords(self):
220        m = re.match('.*<meta name="keywords" content="my1|key2" />', self.html, re.S|re.M)
221        self.assert_(m, 'Keywords not set in')
222
223    def testRobots(self):
224        m = re.match('.*<meta name="robots" content="ALL" />', self.html, re.S|re.M)
225        self.assert_(m, 'Robots not set in')
226
227    def testDistribution(self):
228        m = re.match('.*<meta name="distribution" content="Global" />', self.html, re.S|re.M)
229        self.assert_(m, 'Distribution not set in')
230
231    def testHTMLComments(self):
232        m = re.match('.*<!--\\s*no comments\\s*-->', self.html, re.S|re.M)
233        self.assert_(m, 'Comments not set in')
234
235    def testTagsOrder(self):
236        m = re.search('name="description".+name="keywords"', self.html, re.S|re.M)
237        self.assert_(m, "Meta tags order not supported.")
238
239    def testCustomMetaTags(self):
240        for tag in custom_metatags:
241            m = re.search('<meta name="%(meta_name)s" content="%(meta_content)s" />' % tag, self.html, re.S|re.M)
242            if tag['meta_content']:
243                self.assert_(m, "Custom meta tag %s not applied." % tag['meta_name'])
244            else:
245                self.assert_(not m, "Meta tag %s has no content, but is present in the page." % tag['meta_name'])
246        m = re.search('<meta name="metatag4" content="global_metatag4value" />' , self.html, re.S|re.M)
247        self.assert_(m, "Global custom meta tag %s not applied." % 'metatag4')
248
249    def testDeleteCustomMetaTags(self):
250        self.sp.manage_changeProperties(**{'default_custom_metatags':'metatag1|global_metatag1value'})
251        my_doc = self.my_doc
252        my_doc.qseo_properties_edit(custommetatags=custom_metatags, custommetatags_override=0)
253        html = self.publish(self.abs_path, self.basic_auth).getBody()
254        m = re.search('<meta name="metatag4" content="global_metatag4value" />' , html, re.S|re.M)
255        self.assert_(not m, "Global custom meta tag %s is prosent in the page." % 'metatag4')
256        m = re.search('<meta name="metatag1" content="global_metatag1value" />' , html, re.S|re.M)
257        self.assert_(m, "Global custom meta tag %s is prosent in the page." % 'metatag4')
258
259    def testCanonical(self):
260        m = re.match('.*<link rel="canonical" href="%s" />' % self.canonurl, self.html, re.S|re.M)
261        self.assert_(m, self.canonurl)
262
263    def testDefaultCanonical(self):
264        """Default canonical url mast add document absolute_url
265        """
266        # Delete custom canonical url
267        my_doc = self.portal['my_doc']
268        my_doc._delProperty(id='qSEO_canonical')
269        # Get document without customized canonical url
270        abs_path = "/%s" % my_doc.absolute_url(1)
271        self.html = self.publish(abs_path, self.basic_auth).getBody()
272
273        my_url = my_doc.absolute_url()
274        m = re.match('.*<link rel="canonical" href="%s" />' % my_url, self.html, re.S|re.M)
275        self.assert_(m, my_url)
276
277class TestAdditionalKeywords(PloneTestCase.FunctionalTestCase):
278
279    def afterSetUp(self):
280        self.qi = self.portal.portal_quickinstaller
281        self.qi.installProduct(PRODUCT)
282        self.sp = self.portal.portal_properties.seo_properties
283        self.pu = self.portal.plone_utils
284        #self.portal.changeSkin('Plone Default')
285
286        self.basic_auth = 'portal_manager:secret'
287        uf = self.app.acl_users
288        uf.userFolderAddUser('portal_manager', 'secret', ['Manager'], [])
289        user = uf.getUserById('portal_manager')
290        if not hasattr(user, 'aq_base'):
291            user = user.__of__(uf)
292        newSecurityManager(None, user)
293
294        '''Preparation for functional testing'''
295        self.my_doc = self.portal.invokeFactory('Document', id='my_doc')
296        self.my_doc = self.portal['my_doc']
297
298    def test_additional_keywords_in_configlet(self):
299        quoted_keywords = urllib.quote('foo\nbar')
300        path = self.portal.id+'/@@seo-controlpanel?additionalKeywords:lines=%s&form.submitted=1'%quoted_keywords
301        self.publish(path, self.basic_auth)
302        self.assertEqual(self.sp.additional_keywords, ('foo', 'bar'))
303        self.publish(self.portal.id+'/@@seo-controlpanel?form.submitted=1', self.basic_auth)
304        self.assertEqual(self.sp.additional_keywords, ())
305
306    def test_listMetaTags_empty(self):
307        metatags = self.pu.listMetaTags(self.my_doc)
308        self.assert_('keywords' not in metatags)
309
310    def test_listMetaTags_one(self):       
311        self.my_doc.manage_addProperty('qSEO_keywords', ('foo',), 'lines')
312        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
313        m = re.match('.*<meta\ name="keywords"\ content="foo"\ />', self.html, re.S|re.M)
314        self.assert_(m, "No 'foo' keyword find")
315
316    def test_listMetaTags_two(self):       
317        self.my_doc.manage_addProperty('qSEO_keywords', ('foo', 'bar'), 'lines')
318        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
319        m = re.match('.*<meta\ name="keywords"\ content="foo, bar"\ />', self.html, re.S|re.M)
320        self.assert_(m, "No 'foo, bar' keyword find")
321
322    def test_additional_keywords_in_listMetaTags_empty(self):       
323        self.sp.additional_keywords = ('foo',)
324        metatags = self.pu.listMetaTags(self.my_doc)
325        self.assert_('keywords' not in metatags)
326
327    def test_additional_keywords_in_listMetaTags_one(self):
328        self.my_doc.setText('<p>foo</p>')
329        self.sp.additional_keywords = ('foo',)
330        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
331        m = re.match('.*<meta\ name="keywords"\ content="foo"\ />', self.html, re.S|re.M)
332        self.assert_(m, "No 'foo' keyword find")
333
334    def test_additional_keywords_in_listMetaTags_two(self):
335        self.my_doc.setText('<p>foo bar</p>')
336        self.sp.additional_keywords = ('foo', 'bar')
337        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
338        m = re.match('.*<meta\ name="keywords"\ content="foo, bar"\ />', self.html, re.S|re.M)
339        self.assert_(m, "No 'foo, bar' keyword find")
340
341    def test_additional_keywords_in_listMetaTags_merge(self):
342        self.my_doc.setText('<p>foo bar</p>')
343        self.sp.additional_keywords = ('foo', 'bar')
344        self.my_doc.manage_addProperty('qSEO_keywords', ('baz',), 'lines')
345        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
346        m = re.match('.*<meta\ name="keywords"\ content="baz,\ foo,\ bar"\ />', self.html, re.S|re.M)
347        self.assert_(m, "No 'foo, bar, baz' keyword find")
348
349
350class TestExposeDCMetaTags(PloneTestCase.FunctionalTestCase):
351
352    def afterSetUp(self):
353        self.qi = self.portal.portal_quickinstaller
354        self.sp = self.portal.portal_properties.site_properties
355        self.qi.installProduct(PRODUCT)
356        #self.portal.changeSkin('Plone Default')
357
358        self.basic_auth = 'portal_manager:secret'
359        uf = self.app.acl_users
360        uf.userFolderAddUser('portal_manager', 'secret', ['Manager'], [])
361        user = uf.getUserById('portal_manager')
362        if not hasattr(user, 'aq_base'):
363            user = user.__of__(uf)
364        newSecurityManager(None, user)
365
366        '''Preparation for functional testing'''
367        self.my_doc = self.portal.invokeFactory('Document', id='my_doc')
368        self.my_doc = self.portal['my_doc']
369
370    def test_exposeDCMetaTags_in_configletOn(self):
371        path = self.portal.id+'/@@seo-controlpanel?exposeDCMetaTags=True&form.submitted=1'
372        self.publish(path, self.basic_auth)
373        self.assert_(self.sp.exposeDCMetaTags)
374
375    def test_exposeDCMetaTags_in_configletOff(self):
376        self.publish(self.portal.id+'/@@seo-controlpanel?form.submitted=1', self.basic_auth)
377        self.assert_(not self.sp.exposeDCMetaTags)
378
379    def test_exposeDCMetaTagsPropertyOff(self):
380        self.sp.manage_changeProperties(exposeDCMetaTags = False)
381
382        self.my_doc.qseo_properties_edit()
383        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
384
385        m = re.match('.*<meta content=".*?" name="DC.format" />', self.html, re.S|re.M) or re.match('.*<meta content=".*?" name="DC.distribution" />', self.html, re.S|re.M)
386        self.assert_(not m, 'DC meta tags avaliable when exposeDCMetaTags=False')
387
388    def test_exposeDCMetaTagsPropertyOn(self):
389        self.sp.manage_changeProperties(exposeDCMetaTags = True)
390        self.my_doc.qseo_properties_edit()
391        self.html = str(self.publish(self.portal.id+'/my_doc', self.basic_auth))
392        m = re.match('.*<meta\ name="DC.format"\ content=".*?"\ />', self.html, re.S|re.M) and re.match('.*<meta\ name="DC.type"\ content=".*?"\ />', self.html, re.S|re.M)
393        self.assert_(m, 'DC meta tags not avaliable when createManager=True')
394
395
396class TestMetaTagsDuplication(PloneTestCase.FunctionalTestCase):
397
398    def afterSetUp(self):
399        self.qi = self.portal.portal_quickinstaller
400        self.basic_auth = 'portal_manager:secret'
401        uf = self.app.acl_users
402        uf.userFolderAddUser('portal_manager', 'secret', ['Manager'], [])
403        user = uf.getUserById('portal_manager')
404        if not hasattr(user, 'aq_base'):
405            user = user.__of__(uf)
406        newSecurityManager(None, user)
407
408        '''Preparation for functional testing'''
409        self.my_doc = self.portal.invokeFactory('Document', id='my_doc')
410        self.my_doc = self.portal['my_doc']
411        self.my_doc.update(description="Document description")
412
413    def test_GeneratorMeta(self):
414        # Get document without customized canonical url
415        abs_path = "/%s" % self.my_doc.absolute_url(1)
416        regen = re.compile('<meta\s+[^>]*name=\"generator\"[^>]*>', re.S|re.M)
417
418        # Before product installation
419        html = self.publish(abs_path, self.basic_auth).getBody()
420        lengen = len(regen.findall(html))
421        self.assert_(lengen==1, "There is %d generator meta tag(s) " \
422           "before seoptimizer installation" % lengen)
423
424#         # After PRODUCT installation
425#         self.qi.installProduct(PRODUCT)
426#         html = self.publish(abs_path, self.basic_auth).getBody()
427#         lengen = len(regen.findall(html))
428#         self.assert_(lengen==1, "There is %d generator meta tag(s) " \
429#            "after seoptimizer installation" % lengen)
430
431    def test_DescriptionMeta(self):
432        # Get document without customized canonical url
433        abs_path = "/%s" % self.my_doc.absolute_url(1)
434        regen = re.compile('<meta\s+[^>]*name=\"description\"[^>]*>', re.S|re.M)
435
436        # Before product installation
437        html = self.publish(abs_path, self.basic_auth).getBody()
438        lendesc = len(regen.findall(html))
439        self.assert_(lendesc==1, "There is %d DESCRIPTION meta tag(s) " \
440           "before seoptimizer installation" % lendesc)
441
442#         # After PRODUCT installation
443#         self.qi.installProduct(PRODUCT)
444#         html = self.publish(abs_path, self.basic_auth).getBody()
445#         lendesc = len(regen.findall(html))
446#         self.assert_(lendesc==1, "There is %d DESCRIPTION meta tag(s) " \
447#            "after seoptimizer installation" % lendesc)
448
449class TestBaseURL(PloneTestCase.FunctionalTestCase):
450
451    def afterSetUp(self):
452        self.qi = self.portal.portal_quickinstaller
453        self.qi.installProduct(PRODUCT)
454        #self.portal.changeSkin('Plone Default')
455
456        self.basic_auth = 'portal_manager:secret'
457        uf = self.app.acl_users
458        uf.userFolderAddUser('portal_manager', 'secret', ['Manager'], [])
459        user = uf.getUserById('portal_manager')
460        if not hasattr(user, 'aq_base'):
461            user = user.__of__(uf)
462        newSecurityManager(None, user)
463
464    def test_notFolderBaseURL(self):
465        my_doc = self.portal.invokeFactory('Document', id='my_doc')
466        my_doc = self.portal['my_doc']
467        regen = re.compile('<base\s+[^>]*href=\"([^\"]*)\"[^>]*>', re.S|re.M)
468       
469        path = "/%s" % my_doc.absolute_url(1)
470        html = self.publish(path, self.basic_auth).getBody()
471        burls = regen.findall(html)
472       
473        mydocurl = my_doc.absolute_url()
474        self.assert_(not [1 for burl in burls if not burl==mydocurl],
475           "Wrong BASE URL for document: %s, all must be: %s" % (burls, mydocurl))
476
477    def test_folderBaseURL(self):
478        my_fldr = self.portal.invokeFactory('Folder', id='my_fldr')
479        my_fldr = self.portal['my_fldr']
480        regen = re.compile('<base\s+[^>]*href=\"([^\"]*)\"[^>]*>', re.S|re.M)
481       
482        path = "/%s" % my_fldr.absolute_url(1)
483        html = self.publish(path, self.basic_auth).getBody()
484        burls = regen.findall(html)
485
486        myfldrurl = my_fldr.absolute_url() + '/'
487        self.assert_(not [1 for burl in burls if not burl==myfldrurl],
488           "Wrong BASE URL for folder: %s , all must be : %s" % (burls, myfldrurl))
489
490TESTS = [TestBeforeInstall,
491         TestInstallation,
492         TestResponse,
493         TestExposeDCMetaTags,
494         TestAdditionalKeywords,
495         TestMetaTagsDuplication,
496         TestBaseURL,
497        ]
498
499def test_suite():
500    from unittest import TestSuite, makeSuite
501    suite = TestSuite()
502    for suite_class in TESTS:
503        suite.addTest(makeSuite(suite_class))
504
505    return suite
506
507if __name__ == '__main__':
508    framework()
Note: See TracBrowser for help on using the repository browser.