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

Last change on this file since 6750 was 6747, checked in by uli, 14 years ago

Show remaining loggers only when suspicious.

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