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

Last change on this file since 11040 was 10463, checked in by Henrik Bettermann, 11 years ago

Add browser test for student transcript requests.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 15.0 KB
Line 
1## $Id: testing.py 10463 2013-08-07 09:31:47Z 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.kofa`.
19"""
20import grok
21import doctest
22import logging
23import os.path
24import re
25import tempfile
26import shutil
27import unittest
28import warnings
29import zope.component
30import waeup.kofa
31from zc.async.interfaces import COMPLETED
32from zope.app.testing.functional import (
33    ZCMLLayer, FunctionalTestSetup, getRootFolder, sync, FunctionalTestCase)
34from zope.component import getGlobalSiteManager, queryUtility
35from zope.security.testing import addCheckerPublic
36from zope.testing import renormalizing
37from zope.testing.cleanup import cleanUp
38
39ftesting_zcml = os.path.join(
40    os.path.dirname(waeup.kofa.__file__), 'ftesting.zcml')
41FunctionalLayer = ZCMLLayer(ftesting_zcml, __name__, 'FunctionalLayer',
42                            allow_teardown=True)
43
44def get_all_loggers():
45    """Get the keys of all logger defined globally.
46    """
47    result = logging.root.manager.loggerDict.keys()
48    ws_loggers = [x for x in result if 'waeup.kofa' in x]
49    if ws_loggers:
50        # For debugging: show any remaining loggers from w.k. namespace
51        print "\nLOGGERS: ", ws_loggers
52    return result
53
54def remove_new_loggers(old_loggers):
55    """Remove the loggers except `old_loggers`.
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]
63    for key in sorted(new_loggers, reverse=True):
64        logger = logging.getLogger(key)
65        for handler in logger.handlers:
66            handler.close()
67        del logger
68        del logging.root.manager.loggerDict[key]
69    return
70
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
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():
96    """Try to grok the :mod:`waeup.kofa` package.
97
98    For many tests, even simple ones, we want the components defined
99    somewhere in the :mod:`waeup.kofa` package being registered. While
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
108    The grokking of :mod:`waeup.kofa` is done with warnings disabled.
109
110    Returns ``True`` if grokking was done, ``False`` else.
111
112    .. The following samples should go into Sphinx docs directly....
113
114    Sample
115    ******
116
117    Usage with plain Python testrunners
118    -----------------------------------
119
120    Together with the :func:`setUpZope` and :func:`cleanUpZope`
121    functions we then can do unittests with all components registered
122    and the event dispatcher in place like this::
123
124      import unittest2 as unittest # Want Python 2.7 features
125      from waeup.kofa.testing import (
126        maybe_grok, setUpZope, cleanUpZope,
127        )
128      from waeup.kofa.app import University
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
145
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
156    inside the tests you can expect to have all :mod:`waeup.kofa`
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.
161
162    .. note:: This works only with the default Python testrunners.
163
164         If you use the Zope testrunner (from :mod:`zope.testing`)
165         then you have to use appropriate layers like the
166         :class:`waeup.kofa.testing.KofaUnitTestLayer`.
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
175    .. seealso:: :class:`waeup.kofa.testing.KofaUnitTestLayer`
176
177    """
178    gsm =  getGlobalSiteManager()
179    # If there are any event handlers registered already, we assume
180    # that waeup.kofa was grokked already. There might be a batter
181    # check, though.
182    if len(list(gsm.registeredHandlers())) > 0:
183        return False
184    # Register the zope.Public permission, normally done via ZCML setup.
185    addCheckerPublic()
186    warnings.simplefilter('ignore') # disable (erraneous) warnings
187    grok.testing.grok('waeup.kofa')
188    warnings.simplefilter('default') # reenable warnings
189    return True
190
191def setup_datacenter_conf():
192    """Register a datacenter config utility for non-functional tests.
193    """
194    from waeup.kofa.interfaces import IDataCenterConfig
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
203
204def teardown_datacenter_conf():
205    """Unregister a datacenter config utility for non-functional tests.
206    """
207    from waeup.kofa.interfaces import IDataCenterConfig
208    conf = queryUtility(IDataCenterConfig)
209    if conf is None:
210        return
211    path = conf['path']
212    shutil.rmtree(path)
213    gsm = getGlobalSiteManager()
214    gsm.unregisterUtility(conf, IDataCenterConfig)
215    return
216
217class KofaUnitTestLayer(object):
218    """A layer for tests that groks `waeup.kofa`.
219
220    A Zope test layer that registers all :mod:`waeup.kofa` components
221    before attached tests are run and cleans this registrations up
222    afterwards. Also basic (non-waeup.kofa) components like the event
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).
234
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
240      from waeup.kofa.testing import KofaUnitTestLayer
241
242      class MyTestCase(unittest.TestCase):
243
244          layer = KofaUnitTestLayer
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):
254        #setUpZope(None)
255        grokked = maybe_grok()
256        setup_datacenter_conf()
257        return
258
259    @classmethod
260    def tearDown(cls):
261        teardown_datacenter_conf()
262        cleanUpZope(None)
263        return
264
265
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#:
285#: `<6-DIGITS>`
286#:    matches a sequence of 6 digits. Useful when checking accesscode
287#:    numbers if you don't know the exact (random) code.
288#:
289#: `<YYYY-MM-DD hh:mm:ss>`
290#:    matches any date and time like `2011-05-01 12:01:32`.
291#:
292#: `<DATE-AND-TIME>`
293#:    same like ``<YYYY-MM-DD hh:mm:ss>`` but shorter.
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'), ''),
300    (re.compile(r'httperror_seek_wrapper:'), 'HTTPError:' ),
301    (re.compile('[\d]{6}'), '<6-DIGITS>'),
302    (re.compile('[\d]{10}'), '<10-DIGITS>'),
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>'),
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>'),
307    ])
308
309old_loggers = []
310def setUp(test):
311    old_loggers = get_all_loggers()
312    FunctionalTestSetup().setUp()
313
314def tearDown(test):
315    FunctionalTestSetup().tearDown()
316    remove_new_loggers(old_loggers)
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
335
336optionflags = (
337    doctest.REPORT_NDIFF + doctest.ELLIPSIS + doctest.NORMALIZE_WHITESPACE)
338
339def clear_logger_collector():
340    from zope.component import queryUtility, getGlobalSiteManager
341    from waeup.kofa.interfaces import ILoggerCollector
342    collector = queryUtility(ILoggerCollector)
343    if collector is None:
344        return
345    keys = list(collector.keys())
346    for key in keys:
347        del collector[key]
348    return
349
350class FunctionalTestCase(FunctionalTestCase):
351    """A test case that supports checking output diffs in doctest style.
352    """
353
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)
362        clear_logger_collector()
363        return
364
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
373        :mod:`waeup.kofa.testing`.
374
375        If optional `optionflags` are not given, use ``REPORT_NDIFF``,
376        ``ELLIPSIS``, and ``NORMALIZE_WHITESPACE``.
377
378        .. seealso:: :data:`waeup.kofa.testing.optionflags`
379
380        .. seealso:: :data:`waeup.kofa.testing.checker`
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)
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
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
408    w.k. dir.  So, to get a doctest suite for ``browser.txt`` and
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
416      from waeup.kofa.testing import get_doctest_suite
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
438
439class FakeJob(object):
440    # A job usable for simple async tests
441    status = COMPLETED
442    result = None
443
444    def __init__(self, *args, **kw):
445        self.args = args
446        self.kwargs = kw
447
448class FakeJobManager(object):
449    # A fake job manager for testing async functionality
450
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
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.