source: main/waeup.kofa/trunk/src/waeup/kofa/testing.py @ 9250

Last change on this file since 9250 was 8186, checked in by uli, 13 years ago

Use new helper functions to compute pytz timezones correctly.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 13.9 KB
Line 
1## $Id: testing.py 8186 2012-04-17 00:31:10Z uli $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""Testing support for :mod:`waeup.kofa`.
19"""
20import grok
21import doctest
22import logging
23import os.path
24import re
25import tempfile
26import shutil
27import unittest
28import warnings
29import zope.component
30import waeup.kofa
31from zope.app.testing.functional import (
32    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
33from zope.component import getGlobalSiteManager, queryUtility
34from zope.security.testing import addCheckerPublic
35from zope.testing import renormalizing
36from zope.testing.cleanup import cleanUp
37
38ftesting_zcml = os.path.join(
39    os.path.dirname(waeup.kofa.__file__), 'ftesting.zcml')
40FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
41                            allow_teardown=True)
42
43def get_all_loggers():
44    """Get the keys of all logger defined globally.
45    """
46    result = logging.root.manager.loggerDict.keys()
47    ws_loggers = [x for x in result if 'waeup.kofa' in x]
48    if ws_loggers:
49        # For debugging: show any remaining loggers from w.k. namespace
50        print "\nLOGGERS: ", ws_loggers
51    return result
52
53def remove_new_loggers(old_loggers):
54    """Remove the loggers except `old_loggers`.
55
56    `old_loggers` is a list of logger keys as returned by
57    :func:`get_all_loggers`. All globally registered loggers whose
58    name is not in `old_loggers` is removed.
59    """
60    new_loggers = [key for key in logging.root.manager.loggerDict
61                   if key not in old_loggers]
62    for key in sorted(new_loggers, reverse=True):
63        logger = logging.getLogger(key)
64        for handler in logger.handlers:
65            handler.close()
66        del logger
67        del logging.root.manager.loggerDict[key]
68    return
69
70def remove_logger(name):
71    """Remove logger with name `name`.
72
73    Use is safe. If the logger does not exist nothing happens.
74    """
75    if name in logging.root.manager.loggerDict.keys():
76        del logging.root.manager.loggerDict[name]
77    return
78
79
80def setUpZope(test=None):
81    """Initialize a Zope-compatible environment.
82
83    Currently, we only initialize the event machinery.
84    """
85    zope.component.eventtesting.setUp(test)
86
87def cleanUpZope(test=None):
88    """Clean up Zope-related registrations.
89
90    Cleans up all registrations and the like.
91    """
92    cleanUp()
93
94def maybe_grok():
95    """Try to grok the :mod:`waeup.kofa` package.
96
97    For many tests, even simple ones, we want the components defined
98    somewhere in the :mod:`waeup.kofa` package being registered. While
99    grokking the complete package can become expensive when done many
100    times, we only want to grok if it did not happen
101    before. Furthermore regrokking the whole package makes no sense if
102    done already.
103
104    :func:`maybe_grok` checks whether any eventhandlers are already
105    registered and does nothing in that case.
106
107    The grokking of :mod:`waeup.kofa` is done with warnings disabled.
108
109    Returns ``True`` if grokking was done, ``False`` else.
110
111    .. The following samples should go into Sphinx docs directly....
112
113    Sample
114    ******
115
116    Usage with plain Python testrunners
117    -----------------------------------
118
119    Together with the :func:`setUpZope` and :func:`cleanUpZope`
120    functions we then can do unittests with all components registered
121    and the event dispatcher in place like this::
122
123      import unittest2 as unittest # Want Python 2.7 features
124      from waeup.kofa.testing import (
125        maybe_grok, setUpZope, cleanUpZope,
126        )
127      from waeup.kofa.app import University
128
129      class MyTestCase(unittest.TestCase):
130
131          @classmethod
132          def setUpClass(cls):
133              grokked = maybe_grok()
134              if grokked:
135                  setUpZope(None)
136              return
137
138          @classmethod
139          def tearDownClass(cls):
140              cleanUpZope(None)
141
142          def setUp(self):
143              pass
144
145          def tearDown(self):
146              pass
147
148          def test_jambdata_in_site(self):
149              u = University()
150              self.assertTrue('jambdata' in u.keys())
151              return
152
153    Here the component registration is done only once for the whole
154    :class:`unittest.TestCase` and no ZODB is needed. That means
155    inside the tests you can expect to have all :mod:`waeup.kofa`
156    components (utilities, adapter, events) registered but as objects
157    have here still have no place inside a ZODB things like 'browsing'
158    won't work out of the box. The benefit is the faster test
159    setup/teardown.
160
161    .. note:: This works only with the default Python testrunners.
162
163         If you use the Zope testrunner (from :mod:`zope.testing`)
164         then you have to use appropriate layers like the
165         :class:`waeup.kofa.testing.KofaUnitTestLayer`.
166
167    Usage with :mod:`zope.testing` testrunners
168    ------------------------------------------
169
170    If you use the standard Zope testrunner, classmethods like
171    `setUpClass` are not executed. Instead you have to use a layer
172    like the one defined in this module.
173
174    .. seealso:: :class:`waeup.kofa.testing.KofaUnitTestLayer`
175
176    """
177    gsm =  getGlobalSiteManager()
178    # If there are any event handlers registered already, we assume
179    # that waeup.kofa was grokked already. There might be a batter
180    # check, though.
181    if len(list(gsm.registeredHandlers())) > 0:
182        return False
183    # Register the zope.Public permission, normally done via ZCML setup.
184    addCheckerPublic()
185    warnings.simplefilter('ignore') # disable (erraneous) warnings
186    grok.testing.grok('waeup.kofa')
187    warnings.simplefilter('default') # reenable warnings
188    return True
189
190def setup_datacenter_conf():
191    """Register a datacenter config utility for non-functional tests.
192    """
193    from waeup.kofa.interfaces import IDataCenterConfig
194    conf = queryUtility(IDataCenterConfig)
195    if conf is not None:
196        return
197    path = tempfile.mkdtemp()
198    conf = {'path': path}
199    gsm = getGlobalSiteManager()
200    gsm.registerUtility(conf, IDataCenterConfig)
201    return
202
203def teardown_datacenter_conf():
204    """Unregister a datacenter config utility for non-functional tests.
205    """
206    from waeup.kofa.interfaces import IDataCenterConfig
207    conf = queryUtility(IDataCenterConfig)
208    if conf is None:
209        return
210    path = conf['path']
211    shutil.rmtree(path)
212    gsm = getGlobalSiteManager()
213    gsm.unregisterUtility(conf)
214    return
215
216class KofaUnitTestLayer(object):
217    """A layer for tests that groks `waeup.kofa`.
218
219    A Zope test layer that registers all :mod:`waeup.kofa` components
220    before attached tests are run and cleans this registrations up
221    afterwards. Also basic (non-waeup.kofa) components like the event
222    dispatcher machinery are registered, set up and cleaned up.
223
224    This layer does not provide a complete ZODB setup (and is
225    therefore much faster than complete functional setups) but does
226    only the registrations (which also takes some time, so running
227    this layer is slower than test cases that need none or only a
228    few registrations).
229
230    The registrations are done for all tests the layer is attached to
231    once before all these tests are run (and torn down once
232    afterwards).
233
234    To make use of this layer, you have to write a
235    :mod:`unittest.TestCase` class that provides an attribute called
236    ``layer`` with this class as value like this::
237
238      import unittest
239      from waeup.kofa.testing import KofaUnitTestLayer
240
241      class MyTestCase(unittest.TestCase):
242
243          layer = KofaUnitTestLayer
244
245          # per-test setups and real tests go here...
246          def test_foo(self):
247              self.assertEqual(1, 1)
248              return
249
250    """
251    @classmethod
252    def setUp(cls):
253        #setUpZope(None)
254        grokked = maybe_grok()
255        setup_datacenter_conf()
256        return
257
258    @classmethod
259    def tearDown(cls):
260        cleanUpZope(None)
261        teardown_datacenter_conf()
262        return
263
264
265#: This extended :class:`doctest.OutputChecker` allows the following
266#: additional matches when looking for output diffs:
267#:
268#: `N.NNN seconds`
269#:    matches strings like ``12.123 seconds``
270#:
271#: `HTTPError:`
272#:    matches ``httperror_seek_wrapper:``. This string is output by some
273#:    virtual browsers you might use in functional browser tests to signal
274#:    HTTP error state.
275#:
276#: `1034h`
277#:    is ignored. This sequence of control chars is output by some
278#:    (buggy) testrunners at beginning of output.
279#:
280#: `<10-DIGITS>`
281#:    matches a sequence of 10 digits. Useful when checking accesscode
282#:    numbers if you don't know the exact (random) code.
283#:
284#: `<YYYY-MM-DD hh:mm:ss>`
285#:    matches any date and time like `2011-05-01 12:01:32`.
286#:
287#: `<DATE-AND-TIME>`
288#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
289checker = renormalizing.RENormalizing([
290    # Relevant normalizers from zope.testing.testrunner.tests:
291    (re.compile(r'\d+[.]\d\d\d seconds'), 'N.NNN seconds'),
292    # Our own one to work around
293    # http://reinout.vanrees.org/weblog/2009/07/16/invisible-test-diff.html:
294    (re.compile(r'.*1034h'), ''),
295    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
296    (re.compile('[\d]{10}'), '<10-DIGITS>'),
297    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d [\+\-]\d\d\d\d [^ ]+'),
298     '<YYYY-MM-DD hh:mm:ss TZ>'),
299    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
300    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
301    ])
302
303old_loggers = []
304def setUp(test):
305    old_loggers = get_all_loggers()
306    FunctionalTestSetup().setUp()
307
308def tearDown(test):
309    FunctionalTestSetup().tearDown()
310    remove_new_loggers(old_loggers)
311
312def doctestsuite_for_module(dotted_path):
313    """Create a doctest suite for the module at `dotted_path`.
314    """
315    test = doctest.DocTestSuite(
316        dotted_path,
317        setUp = setUp,
318        tearDown = tearDown,
319        checker = checker,
320        extraglobs = dict(
321            getRootFolder=getRootFolder,
322            sync=sync,),
323        optionflags = (doctest.ELLIPSIS +
324                       doctest.NORMALIZE_WHITESPACE +
325                       doctest.REPORT_NDIFF),
326        )
327    test.layer = FunctionalLayer
328    return test
329
330optionflags = (
331    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
332
333def clear_logger_collector():
334    from zope.component import queryUtility, getGlobalSiteManager
335    from waeup.kofa.interfaces import ILoggerCollector
336    collector = queryUtility(ILoggerCollector)
337    if collector is None:
338        return
339    keys = list(collector.keys())
340    for key in keys:
341        del collector[key]
342    return
343
344class FunctionalTestCase(FunctionalTestCase):
345    """A test case that supports checking output diffs in doctest style.
346    """
347
348    def setUp(self):
349        super(FunctionalTestCase, self).setUp()
350        self.functional_old_loggers = get_all_loggers()
351        return
352
353    def tearDown(self):
354        super(FunctionalTestCase, self).tearDown()
355        remove_new_loggers(self.functional_old_loggers)
356        clear_logger_collector()
357        return
358
359    def assertMatches(self, want, got, checker=checker,
360                      optionflags=optionflags):
361        """Assert that the multiline string `want` matches `got`.
362
363        In `want` you can use shortcuts like ``...`` as in regular doctests.
364
365        If no special `checker` is passed, we use an extended
366        :class:`doctest.OutputChecker` as defined in
367        :mod:`waeup.kofa.testing`.
368
369        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
370        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
371
372        .. seealso:: :data:`waeup.kofa.testing.optionflags`
373
374        .. seealso:: :data:`waeup.kofa.testing.checker`
375        """
376        if checker.check_output(want, got, optionflags):
377            return
378        diff = checker.output_difference(
379            doctest.Example('', want), got, optionflags)
380        self.fail(diff)
381
382class FunctionalTestSetup(FunctionalTestSetup):
383    """A replacement for the zope.app.testing class.
384
385    Removes also loggers.
386    """
387
388    def setUp(self):
389        self.old_loggers = get_all_loggers()
390        super(FunctionalTestSetup, self).setUp()
391        return
392
393    def tearDown(self):
394        super(FunctionalTestSetup, self).tearDown()
395        remove_new_loggers(self.old_loggers)
396        return
397
398def get_doctest_suite(filename_list=[]):
399    """Helper function to create doctest suites for doctests.
400
401    The `filename_list` is a list of filenames relative to the
402    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
403    ``blah.txt`` in the ``browser/`` subpackage you have to pass
404    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
405    so on.
406
407    The returned test suite must be registered somewhere locally for
408    instance by something like:
409
410      from waeup.kofa.testing import get_doctest_suite
411      def test_suite():
412        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
413        return suite
414
415    and that's it.
416    """
417    suite = unittest.TestSuite()
418    for filename in filename_list:
419        path = os.path.join(
420            os.path.dirname(__file__), filename)
421        test = doctest.DocFileSuite(
422            path,
423            module_relative=False,
424            setUp=setUp, tearDown=tearDown,
425            globs = dict(getRootFolder = getRootFolder),
426            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
427            checker = checker,
428            )
429        test.layer = FunctionalLayer
430        suite.addTest(test)
431    return suite
Note: See TracBrowser for help on using the repository browser.