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

Last change on this file since 8182 was 7827, checked in by uli, 13 years ago

Fix this nasty logger problem: when we remove several loggers, we must
do it bottom to top. When we remove waeup.kofa.app after removing
waeup.kofa, we will get a new waeup.kofa logger. This was not visible
with waeup.sirp as the Python-internal sorting picked waeup.sirp after
waeup.sirp.app when looping unsorted over all logger names.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 13.8 KB
RevLine 
[7193]1## $Id: testing.py 7827 2012-03-09 14:13:37Z 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##
[7811]18"""Testing support for :mod:`waeup.kofa`.
[5780]19"""
[6238]20import grok
21import doctest
[6577]22import logging
[3521]23import os.path
[6238]24import re
[7581]25import tempfile
26import shutil
[6735]27import unittest
[5796]28import warnings
[5780]29import zope.component
[7811]30import waeup.kofa
[6238]31from zope.app.testing.functional import (
[6463]32    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
[7581]33from zope.component import getGlobalSiteManager, queryUtility
[5865]34from zope.security.testing import addCheckerPublic
[6238]35from zope.testing import renormalizing
[5796]36from zope.testing.cleanup import cleanUp
[3521]37
38ftesting_zcml = os.path.join(
[7811]39    os.path.dirname(waeup.kofa.__file__), 'ftesting.zcml')
[4789]40FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
41                            allow_teardown=True)
[5139]42
[6577]43def get_all_loggers():
44    """Get the keys of all logger defined globally.
45    """
[6727]46    result = logging.root.manager.loggerDict.keys()
[7811]47    ws_loggers = [x for x in result if 'waeup.kofa' in x]
[6747]48    if ws_loggers:
[7817]49        # For debugging: show any remaining loggers from w.k. namespace
[6747]50        print "\nLOGGERS: ", ws_loggers
[6727]51    return result
[6577]52
53def remove_new_loggers(old_loggers):
[6658]54    """Remove the loggers except `old_loggers`.
[6577]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]
[7827]62    for key in sorted(new_loggers, reverse=True):
[6578]63        logger = logging.getLogger(key)
64        for handler in logger.handlers:
65            handler.close()
66        del logger
[6577]67        del logging.root.manager.loggerDict[key]
68    return
69
[6578]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
[5780]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():
[7811]95    """Try to grok the :mod:`waeup.kofa` package.
[5780]96
97    For many tests, even simple ones, we want the components defined
[7811]98    somewhere in the :mod:`waeup.kofa` package being registered. While
[5780]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
[7811]107    The grokking of :mod:`waeup.kofa` is done with warnings disabled.
[5780]108
109    Returns ``True`` if grokking was done, ``False`` else.
110
[5796]111    .. The following samples should go into Sphinx docs directly....
[6237]112
[5780]113    Sample
114    ******
[5796]115
116    Usage with plain Python testrunners
117    -----------------------------------
[6237]118
[5780]119    Together with the :func:`setUpZope` and :func:`cleanUpZope`
120    functions we then can do unittests with all components registered
[5796]121    and the event dispatcher in place like this::
[5780]122
123      import unittest2 as unittest # Want Python 2.7 features
[7811]124      from waeup.kofa.testing import (
[5780]125        maybe_grok, setUpZope, cleanUpZope,
126        )
[7811]127      from waeup.kofa.app import University
[5780]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
[6237]144
[5780]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
[7811]155    inside the tests you can expect to have all :mod:`waeup.kofa`
[5780]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.
[5796]160
161    .. note:: This works only with the default Python testrunners.
[6237]162
[5796]163         If you use the Zope testrunner (from :mod:`zope.testing`)
164         then you have to use appropriate layers like the
[7819]165         :class:`waeup.kofa.testing.KofaUnitTestLayer`.
[5796]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
[7819]174    .. seealso:: :class:`waeup.kofa.testing.KofaUnitTestLayer`
[6237]175
[5780]176    """
177    gsm =  getGlobalSiteManager()
178    # If there are any event handlers registered already, we assume
[7811]179    # that waeup.kofa was grokked already. There might be a batter
[5780]180    # check, though.
181    if len(list(gsm.registeredHandlers())) > 0:
182        return False
[5865]183    # Register the zope.Public permission, normally done via ZCML setup.
184    addCheckerPublic()
[5780]185    warnings.simplefilter('ignore') # disable (erraneous) warnings
[7811]186    grok.testing.grok('waeup.kofa')
[5780]187    warnings.simplefilter('default') # reenable warnings
188    return True
[5796]189
[7581]190def setup_datacenter_conf():
191    """Register a datacenter config utility for non-functional tests.
192    """
[7811]193    from waeup.kofa.interfaces import IDataCenterConfig
[7581]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
[5796]202
[7581]203def teardown_datacenter_conf():
204    """Unregister a datacenter config utility for non-functional tests.
205    """
[7811]206    from waeup.kofa.interfaces import IDataCenterConfig
[7581]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
[7819]216class KofaUnitTestLayer(object):
[7811]217    """A layer for tests that groks `waeup.kofa`.
[5796]218
[7811]219    A Zope test layer that registers all :mod:`waeup.kofa` components
[5796]220    before attached tests are run and cleans this registrations up
[7811]221    afterwards. Also basic (non-waeup.kofa) components like the event
[5796]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).
[6237]233
[5796]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
[7819]239      from waeup.kofa.testing import KofaUnitTestLayer
[6237]240
[5796]241      class MyTestCase(unittest.TestCase):
242
[7819]243          layer = KofaUnitTestLayer
[5796]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):
[5865]253        #setUpZope(None)
[5796]254        grokked = maybe_grok()
[7581]255        setup_datacenter_conf()
[5796]256        return
257
258    @classmethod
259    def tearDown(cls):
260        cleanUpZope(None)
[7581]261        teardown_datacenter_conf()
262        return
[6238]263
[7581]264
[6463]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#:
[6466]284#: `<YYYY-MM-DD hh:mm:ss>`
[6463]285#:    matches any date and time like `2011-05-01 12:01:32`.
286#:
287#: `<DATE-AND-TIME>`
[6466]288#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
[6238]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'), ''),
[6463]295    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
296    (re.compile('[\d]{10}'), '<10-DIGITS>'),
[6466]297    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
298    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
[6238]299    ])
300
[6727]301old_loggers = []
[6238]302def setUp(test):
[6727]303    old_loggers = get_all_loggers()
[6238]304    FunctionalTestSetup().setUp()
305
306def tearDown(test):
307    FunctionalTestSetup().tearDown()
[6727]308    remove_new_loggers(old_loggers)
[6238]309
310def doctestsuite_for_module(dotted_path):
311    """Create a doctest suite for the module at `dotted_path`.
312    """
313    test = doctest.DocTestSuite(
314        dotted_path,
315        setUp = setUp,
316        tearDown = tearDown,
317        checker = checker,
318        extraglobs = dict(
319            getRootFolder=getRootFolder,
320            sync=sync,),
321        optionflags = (doctest.ELLIPSIS +
322                       doctest.NORMALIZE_WHITESPACE +
323                       doctest.REPORT_NDIFF),
324        )
325    test.layer = FunctionalLayer
326    return test
[6463]327
328optionflags = (
329    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
330
[6754]331def clear_logger_collector():
332    from zope.component import queryUtility, getGlobalSiteManager
[7811]333    from waeup.kofa.interfaces import ILoggerCollector
[6754]334    collector = queryUtility(ILoggerCollector)
335    if collector is None:
336        return
[6980]337    keys = list(collector.keys())
[6754]338    for key in keys:
339        del collector[key]
340    return
341
[6463]342class FunctionalTestCase(FunctionalTestCase):
343    """A test case that supports checking output diffs in doctest style.
344    """
345
[6578]346    def setUp(self):
347        super(FunctionalTestCase, self).setUp()
348        self.functional_old_loggers = get_all_loggers()
349        return
350
351    def tearDown(self):
352        super(FunctionalTestCase, self).tearDown()
353        remove_new_loggers(self.functional_old_loggers)
[6754]354        clear_logger_collector()
[6578]355        return
356
[6463]357    def assertMatches(self, want, got, checker=checker,
358                      optionflags=optionflags):
359        """Assert that the multiline string `want` matches `got`.
360
361        In `want` you can use shortcuts like ``...`` as in regular doctests.
362
363        If no special `checker` is passed, we use an extended
364        :class:`doctest.OutputChecker` as defined in
[7811]365        :mod:`waeup.kofa.testing`.
[6463]366
367        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
368        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
369
[7811]370        .. seealso:: :data:`waeup.kofa.testing.optionflags`
[6463]371
[7811]372        .. seealso:: :data:`waeup.kofa.testing.checker`
[6463]373        """
374        if checker.check_output(want, got, optionflags):
375            return
376        diff = checker.output_difference(
377            doctest.Example('', want), got, optionflags)
378        self.fail(diff)
[6727]379
380class FunctionalTestSetup(FunctionalTestSetup):
381    """A replacement for the zope.app.testing class.
382
383    Removes also loggers.
384    """
385
386    def setUp(self):
387        self.old_loggers = get_all_loggers()
388        super(FunctionalTestSetup, self).setUp()
389        return
390
391    def tearDown(self):
392        super(FunctionalTestSetup, self).tearDown()
393        remove_new_loggers(self.old_loggers)
394        return
[6735]395
396def get_doctest_suite(filename_list=[]):
397    """Helper function to create doctest suites for doctests.
398
399    The `filename_list` is a list of filenames relative to the
[7817]400    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
[6735]401    ``blah.txt`` in the ``browser/`` subpackage you have to pass
402    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
403    so on.
404
405    The returned test suite must be registered somewhere locally for
406    instance by something like:
407
[7811]408      from waeup.kofa.testing import get_doctest_suite
[6735]409      def test_suite():
410        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
411        return suite
412
413    and that's it.
414    """
415    suite = unittest.TestSuite()
416    for filename in filename_list:
417        path = os.path.join(
418            os.path.dirname(__file__), filename)
419        test = doctest.DocFileSuite(
420            path,
421            module_relative=False,
422            setUp=setUp, tearDown=tearDown,
423            globs = dict(getRootFolder = getRootFolder),
424            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
425            checker = checker,
426            )
427        test.layer = FunctionalLayer
428        suite.addTest(test)
429    return suite
Note: See TracBrowser for help on using the repository browser.