source: main/waeup.kofa/trunk/src/waeup/kofa/testing.py @ 9421

Last change on this file since 9421 was 9343, checked in by uli, 12 years ago

Fix FakeJobManager? to work with different instances in different tests.

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