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

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

Show remaining loggers only when suspicious.

  • Property svn:eol-style set to native
File size: 11.9 KB
Line 
1"""Testing support for :mod:`waeup.sirp`.
2"""
3import grok
4import doctest
5import logging
6import os.path
7import re
8import unittest
9import warnings
10import zope.component
11import waeup.sirp
12from zope.app.testing.functional import (
13    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
14from zope.component import getGlobalSiteManager
15from zope.security.testing import addCheckerPublic
16from zope.testing import renormalizing
17from zope.testing.cleanup import cleanUp
18
19ftesting_zcml = os.path.join(
20    os.path.dirname(waeup.sirp.__file__), 'ftesting.zcml')
21FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
22                            allow_teardown=True)
23
24def get_all_loggers():
25    """Get the keys of all logger defined globally.
26    """
27    result = logging.root.manager.loggerDict.keys()
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
32    return result
33
34def remove_new_loggers(old_loggers):
35    """Remove the loggers except `old_loggers`.
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:
44        logger = logging.getLogger(key)
45        for handler in logger.handlers:
46            handler.close()
47        del logger
48        del logging.root.manager.loggerDict[key]
49    return
50
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
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
92    .. The following samples should go into Sphinx docs directly....
93
94    Sample
95    ******
96
97    Usage with plain Python testrunners
98    -----------------------------------
99
100    Together with the :func:`setUpZope` and :func:`cleanUpZope`
101    functions we then can do unittests with all components registered
102    and the event dispatcher in place like this::
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
125
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.
141
142    .. note:: This works only with the default Python testrunners.
143
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`
156
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
164    # Register the zope.Public permission, normally done via ZCML setup.
165    addCheckerPublic()
166    warnings.simplefilter('ignore') # disable (erraneous) warnings
167    grok.testing.grok('waeup.sirp')
168    warnings.simplefilter('default') # reenable warnings
169    return True
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).
189
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
196
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):
209        #setUpZope(None)
210        grokked = maybe_grok()
211        if grokked:
212            pass
213            #setUpZope(None)
214        return
215
216    @classmethod
217    def tearDown(cls):
218        cleanUpZope(None)
219
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#:
239#: `<YYYY-MM-DD hh:mm:ss>`
240#:    matches any date and time like `2011-05-01 12:01:32`.
241#:
242#: `<DATE-AND-TIME>`
243#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
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'), ''),
250    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
251    (re.compile('[\d]{10}'), '<10-DIGITS>'),
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>'),
254    ])
255
256old_loggers = []
257def setUp(test):
258    old_loggers = get_all_loggers()
259    FunctionalTestSetup().setUp()
260
261def tearDown(test):
262    FunctionalTestSetup().tearDown()
263    remove_new_loggers(old_loggers)
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
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
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
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
313        .. seealso:: :data:`waeup.sirp.testing.optionflags`
314
315        .. seealso:: :data:`waeup.sirp.testing.checker`
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)
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
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.