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

Last change on this file since 10436 was 9483, checked in by uli, 12 years ago

Also fake the args and kwargs that are part of real jobs.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 15.0 KB
RevLine 
[7193]1## $Id: testing.py 9483 2012-10-31 12:01:39Z 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#:
[9425]285#: `<6-DIGITS>`
286#:    matches a sequence of 10 digits. Useful when checking accesscode
287#:    numbers if you don't know the exact (random) code.
288#:
[6466]289#: `<YYYY-MM-DD hh:mm:ss>`
[6463]290#:    matches any date and time like `2011-05-01 12:01:32`.
291#:
292#: `<DATE-AND-TIME>`
[6466]293#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
[6238]294checker = renormalizing.RENormalizing([
295    # Relevant normalizers from zope.testing.testrunner.tests:
296    (re.compile(r'\d+[.]\d\d\d seconds'), 'N.NNN seconds'),
297    # Our own one to work around
298    # http://reinout.vanrees.org/weblog/2009/07/16/invisible-test-diff.html:
299    (re.compile(r'.*1034h'), ''),
[6463]300    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
[9425]301    (re.compile('[\d]{6}'), '<6-DIGITS>'),
[6463]302    (re.compile('[\d]{10}'), '<10-DIGITS>'),
[8186]303    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d [\+\-]\d\d\d\d [^ ]+'),
304     '<YYYY-MM-DD hh:mm:ss TZ>'),
[6466]305    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<YYYY-MM-DD hh:mm:ss>'),
306    (re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d'), '<DATETIME>'),
[6238]307    ])
308
[6727]309old_loggers = []
[6238]310def setUp(test):
[6727]311    old_loggers = get_all_loggers()
[6238]312    FunctionalTestSetup().setUp()
313
314def tearDown(test):
315    FunctionalTestSetup().tearDown()
[6727]316    remove_new_loggers(old_loggers)
[6238]317
318def doctestsuite_for_module(dotted_path):
319    """Create a doctest suite for the module at `dotted_path`.
320    """
321    test = doctest.DocTestSuite(
322        dotted_path,
323        setUp = setUp,
324        tearDown = tearDown,
325        checker = checker,
326        extraglobs = dict(
327            getRootFolder=getRootFolder,
328            sync=sync,),
329        optionflags = (doctest.ELLIPSIS +
330                       doctest.NORMALIZE_WHITESPACE +
331                       doctest.REPORT_NDIFF),
332        )
333    test.layer = FunctionalLayer
334    return test
[6463]335
336optionflags = (
337    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
338
[6754]339def clear_logger_collector():
340    from zope.component import queryUtility, getGlobalSiteManager
[7811]341    from waeup.kofa.interfaces import ILoggerCollector
[6754]342    collector = queryUtility(ILoggerCollector)
343    if collector is None:
344        return
[6980]345    keys = list(collector.keys())
[6754]346    for key in keys:
347        del collector[key]
348    return
349
[6463]350class FunctionalTestCase(FunctionalTestCase):
351    """A test case that supports checking output diffs in doctest style.
352    """
353
[6578]354    def setUp(self):
355        super(FunctionalTestCase, self).setUp()
356        self.functional_old_loggers = get_all_loggers()
357        return
358
359    def tearDown(self):
360        super(FunctionalTestCase, self).tearDown()
361        remove_new_loggers(self.functional_old_loggers)
[6754]362        clear_logger_collector()
[6578]363        return
364
[6463]365    def assertMatches(self, want, got, checker=checker,
366                      optionflags=optionflags):
367        """Assert that the multiline string `want` matches `got`.
368
369        In `want` you can use shortcuts like ``...`` as in regular doctests.
370
371        If no special `checker` is passed, we use an extended
372        :class:`doctest.OutputChecker` as defined in
[7811]373        :mod:`waeup.kofa.testing`.
[6463]374
375        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
376        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
377
[7811]378        .. seealso:: :data:`waeup.kofa.testing.optionflags`
[6463]379
[7811]380        .. seealso:: :data:`waeup.kofa.testing.checker`
[6463]381        """
382        if checker.check_output(want, got, optionflags):
383            return
384        diff = checker.output_difference(
385            doctest.Example('', want), got, optionflags)
386        self.fail(diff)
[6727]387
388class FunctionalTestSetup(FunctionalTestSetup):
389    """A replacement for the zope.app.testing class.
390
391    Removes also loggers.
392    """
393
394    def setUp(self):
395        self.old_loggers = get_all_loggers()
396        super(FunctionalTestSetup, self).setUp()
397        return
398
399    def tearDown(self):
400        super(FunctionalTestSetup, self).tearDown()
401        remove_new_loggers(self.old_loggers)
402        return
[6735]403
404def get_doctest_suite(filename_list=[]):
405    """Helper function to create doctest suites for doctests.
406
407    The `filename_list` is a list of filenames relative to the
[7817]408    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
[6735]409    ``blah.txt`` in the ``browser/`` subpackage you have to pass
410    ``filename_list=['browser/browser.txt','browser/blah.txt']`` and
411    so on.
412
413    The returned test suite must be registered somewhere locally for
414    instance by something like:
415
[7811]416      from waeup.kofa.testing import get_doctest_suite
[6735]417      def test_suite():
418        suite = get_doctest_suite(['mypkg/foo.txt', 'mypkg/bar.txt'])
419        return suite
420
421    and that's it.
422    """
423    suite = unittest.TestSuite()
424    for filename in filename_list:
425        path = os.path.join(
426            os.path.dirname(__file__), filename)
427        test = doctest.DocFileSuite(
428            path,
429            module_relative=False,
430            setUp=setUp, tearDown=tearDown,
431            globs = dict(getRootFolder = getRootFolder),
432            optionflags = doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE,
433            checker = checker,
434            )
435        test.layer = FunctionalLayer
436        suite.addTest(test)
437    return suite
[9342]438
439class FakeJob(object):
440    # A job usable for simple async tests
441    status = COMPLETED
442    result = None
443
[9483]444    def __init__(self, *args, **kw):
445        self.args = args
446        self.kwargs = kw
447
[9342]448class FakeJobManager(object):
449    # A fake job manager for testing async functionality
450
[9343]451    def __init__(self):
452        # make sure each instance maintains an own set of jobs/nums.
453        self._jobs = dict()
454        self._curr_num = 1
455
[9342]456    def get(self, job_id):
457        if job_id == '3':
458            return FakeJob()
459        return self._jobs.get(job_id, None)
460
461    def put(self, job):
462        num = str(self._curr_num)
463        self._jobs[num] = job
464        self._curr_num += 1
465        return num
466
467    def remove(self, job_id, site):
468        if job_id in self._jobs:
469            del self._jobs[job_id]
470        return
Note: See TracBrowser for help on using the repository browser.