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

Last change on this file since 7494 was 7321, checked in by Henrik Bettermann, 13 years ago

Replace the term 'WAeUP' by SIRP which is a WAeUP product.

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