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

Last change on this file since 7906 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
Line 
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##
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'), '<YYYY-MM-DD hh:mm:ss>'),
298    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
299    ])
300
301old_loggers = []
302def setUp(test):
303    old_loggers = get_all_loggers()
304    FunctionalTestSetup().setUp()
305
306def tearDown(test):
307    FunctionalTestSetup().tearDown()
308    remove_new_loggers(old_loggers)
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
327
328optionflags = (
329    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
330
331def clear_logger_collector():
332    from zope.component import queryUtility, getGlobalSiteManager
333    from waeup.kofa.interfaces import ILoggerCollector
334    collector = queryUtility(ILoggerCollector)
335    if collector is None:
336        return
337    keys = list(collector.keys())
338    for key in keys:
339        del collector[key]
340    return
341
342class FunctionalTestCase(FunctionalTestCase):
343    """A test case that supports checking output diffs in doctest style.
344    """
345
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)
354        clear_logger_collector()
355        return
356
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
365        :mod:`waeup.kofa.testing`.
366
367        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
368        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
369
370        .. seealso:: :data:`waeup.kofa.testing.optionflags`
371
372        .. seealso:: :data:`waeup.kofa.testing.checker`
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)
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
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
400    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
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
408      from waeup.kofa.testing import get_doctest_suite
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.