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

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

Add two logging-related test helpers: a function to retrieve all
registered logger names and another one that removes loggers from
global (Python) registry, if they are not contained in the list. In
tests you can use the former to determine the set of pre-test loggers
and the latter to remove all loggers added during tests.

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