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

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

Add another logger-aware replacement for stock zope.app.testing stuff.

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