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

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

Logger-related stuff.

  • Property svn:eol-style set to native
File size: 12.3 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
286def clear_logger_collector():
287    from zope.component import queryUtility, getGlobalSiteManager
288    from waeup.sirp.interfaces import ILoggerCollector
289    collector = queryUtility(ILoggerCollector)
290    if collector is None:
291        return
292    keys = collector.keys()
293    for key in keys:
294        del collector[key]
295    return
296
297class FunctionalTestCase(FunctionalTestCase):
298    """A test case that supports checking output diffs in doctest style.
299    """
300
301    def setUp(self):
302        super(FunctionalTestCase, self).setUp()
303        self.functional_old_loggers = get_all_loggers()
304        return
305
306    def tearDown(self):
307        super(FunctionalTestCase, self).tearDown()
308        remove_new_loggers(self.functional_old_loggers)
309        clear_logger_collector()
310        return
311
312    def assertMatches(self, want, got, checker=checker,
313                      optionflags=optionflags):
314        """Assert that the multiline string `want` matches `got`.
315
316        In `want` you can use shortcuts like ``...`` as in regular doctests.
317
318        If no special `checker` is passed, we use an extended
319        :class:`doctest.OutputChecker` as defined in
320        :mod:`waeup.sirp.testing`.
321
322        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
323        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
324
325        .. seealso:: :data:`waeup.sirp.testing.optionflags`
326
327        .. seealso:: :data:`waeup.sirp.testing.checker`
328        """
329        if checker.check_output(want, got, optionflags):
330            return
331        diff = checker.output_difference(
332            doctest.Example('', want), got, optionflags)
333        self.fail(diff)
334
335class FunctionalTestSetup(FunctionalTestSetup):
336    """A replacement for the zope.app.testing class.
337
338    Removes also loggers.
339    """
340
341    def setUp(self):
342        self.old_loggers = get_all_loggers()
343        super(FunctionalTestSetup, self).setUp()
344        return
345
346    def tearDown(self):
347        super(FunctionalTestSetup, self).tearDown()
348        remove_new_loggers(self.old_loggers)
349        return
350
351def get_doctest_suite(filename_list=[]):
352    """Helper function to create doctest suites for doctests.
353
354    The `filename_list` is a list of filenames relative to the
355    w.s. dir.  So, to get a doctest suite for ``browser.txt`` and
356    ``blah.txt`` in the ``browser/`` subpackage you have to pass
357    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
358    so on.
359
360    The returned test suite must be registered somewhere locally for
361    instance by something like:
362
363      from waeup.sirp.testing import get_doctest_suite
364      def test_suite():
365        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
366        return suite
367
368    and that's it.
369    """
370    suite = unittest.TestSuite()
371    for filename in filename_list:
372        path = os.path.join(
373            os.path.dirname(__file__), filename)
374        test = doctest.DocFileSuite(
375            path,
376            module_relative=False,
377            setUp=setUp, tearDown=tearDown,
378            globs = dict(getRootFolder = getRootFolder),
379            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
380            checker = checker,
381            )
382        test.layer = FunctionalLayer
383        suite.addTest(test)
384    return suite
Note: See TracBrowser for help on using the repository browser.