source: main/waeup.sirp/trunk/src/waeup/sirp/testing.py @ 6620

Last change on this file since 6620 was 6578, checked in by uli, 13 years ago

Implement a new logging technique and use it.
Details are explained in waeup.sirp.utils.logger.

  • Property svn:eol-style set to native
File size: 10.0 KB
RevLine 
[5780]1"""Testing support for :mod:`waeup.sirp`.
2"""
[6238]3import grok
4import doctest
[6577]5import logging
[3521]6import os.path
[6238]7import re
[5796]8import warnings
[5780]9import zope.component
[4920]10import waeup.sirp
[6238]11from zope.app.testing.functional import (
[6463]12    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
[5780]13from zope.component import getGlobalSiteManager
[5865]14from zope.security.testing import addCheckerPublic
[6238]15from zope.testing import renormalizing
[5796]16from zope.testing.cleanup import cleanUp
[3521]17
18ftesting_zcml = os.path.join(
[4920]19    os.path.dirname(waeup.sirp.__file__), 'ftesting.zcml')
[4789]20FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
21                            allow_teardown=True)
[5139]22
[6577]23def get_all_loggers():
24    """Get the keys of all logger defined globally.
25    """
26    return logging.root.manager.loggerDict.keys()
27
28def remove_new_loggers(old_loggers):
29    """Remove the loggers in `old_loggers`.
30
31    `old_loggers` is a list of logger keys as returned by
32    :func:`get_all_loggers`. All globally registered loggers whose
33    name is not in `old_loggers` is removed.
34    """
35    new_loggers = [key for key in logging.root.manager.loggerDict
36                   if key not in old_loggers]
37    for key in new_loggers:
[6578]38        logger = logging.getLogger(key)
39        for handler in logger.handlers:
40            handler.close()
41        del logger
[6577]42        del logging.root.manager.loggerDict[key]
43    return
44
[6578]45def remove_logger(name):
46    """Remove logger with name `name`.
47
48    Use is safe. If the logger does not exist nothing happens.
49    """
50    if name in logging.root.manager.loggerDict.keys():
51        del logging.root.manager.loggerDict[name]
52    return
53
54
[5780]55def setUpZope(test=None):
56    """Initialize a Zope-compatible environment.
57
58    Currently, we only initialize the event machinery.
59    """
60    zope.component.eventtesting.setUp(test)
61
62def cleanUpZope(test=None):
63    """Clean up Zope-related registrations.
64
65    Cleans up all registrations and the like.
66    """
67    cleanUp()
68
69def maybe_grok():
70    """Try to grok the :mod:`waeup.sirp` package.
71
72    For many tests, even simple ones, we want the components defined
73    somewhere in the :mod:`waeup.sirp` package being registered. While
74    grokking the complete package can become expensive when done many
75    times, we only want to grok if it did not happen
76    before. Furthermore regrokking the whole package makes no sense if
77    done already.
78
79    :func:`maybe_grok` checks whether any eventhandlers are already
80    registered and does nothing in that case.
81
82    The grokking of :mod:`waeup.sirp` is done with warnings disabled.
83
84    Returns ``True`` if grokking was done, ``False`` else.
85
[5796]86    .. The following samples should go into Sphinx docs directly....
[6237]87
[5780]88    Sample
89    ******
[5796]90
91    Usage with plain Python testrunners
92    -----------------------------------
[6237]93
[5780]94    Together with the :func:`setUpZope` and :func:`cleanUpZope`
95    functions we then can do unittests with all components registered
[5796]96    and the event dispatcher in place like this::
[5780]97
98      import unittest2 as unittest # Want Python 2.7 features
99      from waeup.sirp.testing import (
100        maybe_grok, setUpZope, cleanUpZope,
101        )
102      from waeup.sirp.app import University
103
104      class MyTestCase(unittest.TestCase):
105
106          @classmethod
107          def setUpClass(cls):
108              grokked = maybe_grok()
109              if grokked:
110                  setUpZope(None)
111              return
112
113          @classmethod
114          def tearDownClass(cls):
115              cleanUpZope(None)
116
117          def setUp(self):
118              pass
[6237]119
[5780]120          def tearDown(self):
121              pass
122
123          def test_jambdata_in_site(self):
124              u = University()
125              self.assertTrue('jambdata' in u.keys())
126              return
127
128    Here the component registration is done only once for the whole
129    :class:`unittest.TestCase` and no ZODB is needed. That means
130    inside the tests you can expect to have all :mod:`waeup.sirp`
131    components (utilities, adapter, events) registered but as objects
132    have here still have no place inside a ZODB things like 'browsing'
133    won't work out of the box. The benefit is the faster test
134    setup/teardown.
[5796]135
136    .. note:: This works only with the default Python testrunners.
[6237]137
[5796]138         If you use the Zope testrunner (from :mod:`zope.testing`)
139         then you have to use appropriate layers like the
140         :class:`waeup.sirp.testing.WAeUPSIRPUnitTestLayer`.
141
142    Usage with :mod:`zope.testing` testrunners
143    ------------------------------------------
144
145    If you use the standard Zope testrunner, classmethods like
146    `setUpClass` are not executed. Instead you have to use a layer
147    like the one defined in this module.
148
149    .. seealso:: :class:`waeup.sirp.testing.WAeUPSIRPUnitTestLayer`
[6237]150
[5780]151    """
152    gsm =  getGlobalSiteManager()
153    # If there are any event handlers registered already, we assume
154    # that waeup.sirp was grokked already. There might be a batter
155    # check, though.
156    if len(list(gsm.registeredHandlers())) > 0:
157        return False
[5865]158    # Register the zope.Public permission, normally done via ZCML setup.
159    addCheckerPublic()
[5780]160    warnings.simplefilter('ignore') # disable (erraneous) warnings
161    grok.testing.grok('waeup.sirp')
162    warnings.simplefilter('default') # reenable warnings
163    return True
[5796]164
165
166class WAeUPSIRPUnitTestLayer(object):
167    """A layer for tests that groks `waeup.sirp`.
168
169    A Zope test layer that registers all :mod:`waeup.sirp` components
170    before attached tests are run and cleans this registrations up
171    afterwards. Also basic (non-waeup.sirp) components like the event
172    dispatcher machinery are registered, set up and cleaned up.
173
174    This layer does not provide a complete ZODB setup (and is
175    therefore much faster than complete functional setups) but does
176    only the registrations (which also takes some time, so running
177    this layer is slower than test cases that need none or only a
178    few registrations).
179
180    The registrations are done for all tests the layer is attached to
181    once before all these tests are run (and torn down once
182    afterwards).
[6237]183
[5796]184    To make use of this layer, you have to write a
185    :mod:`unittest.TestCase` class that provides an attribute called
186    ``layer`` with this class as value like this::
187
188      import unittest
189      from waeup.sirp.testing import WAeUPSIRPUnitTestLayer
[6237]190
[5796]191      class MyTestCase(unittest.TestCase):
192
193          layer = WAeUPSIRPUnitTestLayer
194
195          # per-test setups and real tests go here...
196          def test_foo(self):
197              self.assertEqual(1, 1)
198              return
199
200    """
201    @classmethod
202    def setUp(cls):
[5865]203        #setUpZope(None)
[5796]204        grokked = maybe_grok()
205        if grokked:
[5865]206            pass
207            #setUpZope(None)
[5796]208        return
209
210    @classmethod
211    def tearDown(cls):
212        cleanUpZope(None)
[6238]213
[6463]214#: This extended :class:`doctest.OutputChecker` allows the following
215#: additional matches when looking for output diffs:
216#:
217#: `N.NNN seconds`
218#:    matches strings like ``12.123 seconds``
219#:
220#: `HTTPError:`
221#:    matches ``httperror_seek_wrapper:``. This string is output by some
222#:    virtual browsers you might use in functional browser tests to signal
223#:    HTTP error state.
224#:
225#: `1034h`
226#:    is ignored. This sequence of control chars is output by some
227#:    (buggy) testrunners at beginning of output.
228#:
229#: `<10-DIGITS>`
230#:    matches a sequence of 10 digits. Useful when checking accesscode
231#:    numbers if you don't know the exact (random) code.
232#:
[6466]233#: `<YYYY-MM-DD hh:mm:ss>`
[6463]234#:    matches any date and time like `2011-05-01 12:01:32`.
235#:
236#: `<DATE-AND-TIME>`
[6466]237#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
[6238]238checker = renormalizing.RENormalizing([
239    # Relevant normalizers from zope.testing.testrunner.tests:
240    (re.compile(r'\d+[.]\d\d\d seconds'), 'N.NNN seconds'),
241    # Our own one to work around
242    # http://reinout.vanrees.org/weblog/2009/07/16/invisible-test-diff.html:
243    (re.compile(r'.*1034h'), ''),
[6463]244    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
245    (re.compile('[\d]{10}'), '<10-DIGITS>'),
[6466]246    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
247    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
[6238]248    ])
249
250def setUp(test):
251    FunctionalTestSetup().setUp()
252
253def tearDown(test):
254    FunctionalTestSetup().tearDown()
255
256def doctestsuite_for_module(dotted_path):
257    """Create a doctest suite for the module at `dotted_path`.
258    """
259    test = doctest.DocTestSuite(
260        dotted_path,
261        setUp = setUp,
262        tearDown = tearDown,
263        checker = checker,
264        extraglobs = dict(
265            getRootFolder=getRootFolder,
266            sync=sync,),
267        optionflags = (doctest.ELLIPSIS +
268                       doctest.NORMALIZE_WHITESPACE +
269                       doctest.REPORT_NDIFF),
270        )
271    test.layer = FunctionalLayer
272    return test
[6463]273
274optionflags = (
275    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
276
277class FunctionalTestCase(FunctionalTestCase):
278    """A test case that supports checking output diffs in doctest style.
279    """
280
[6578]281    def setUp(self):
282        super(FunctionalTestCase, self).setUp()
283        self.functional_old_loggers = get_all_loggers()
284        return
285
286    def tearDown(self):
287        super(FunctionalTestCase, self).tearDown()
288        remove_new_loggers(self.functional_old_loggers)
289        return
290
[6463]291    def assertMatches(self, want, got, checker=checker,
292                      optionflags=optionflags):
293        """Assert that the multiline string `want` matches `got`.
294
295        In `want` you can use shortcuts like ``...`` as in regular doctests.
296
297        If no special `checker` is passed, we use an extended
298        :class:`doctest.OutputChecker` as defined in
299        :mod:`waeup.sirp.testing`.
300
301        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
302        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
303
[6464]304        .. seealso:: :data:`waeup.sirp.testing.optionflags`
[6463]305
[6464]306        .. seealso:: :data:`waeup.sirp.testing.checker`
[6463]307        """
308        if checker.check_output(want, got, optionflags):
309            return
310        diff = checker.output_difference(
311            doctest.Example('', want), got, optionflags)
312        self.fail(diff)
Note: See TracBrowser for help on using the repository browser.