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

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

Add convenience function to setup doctests more easily.

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