~barry/mailman/events-and-web

« back to all changes in this revision

Viewing changes to src/mailman/testing/layers.py

  • Committer: klm
  • Date: 1998-01-07 21:21:35 UTC
  • Revision ID: vcs-imports@canonical.com-19980107212135-sv0y521ps0xye37r
Initial revision

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008-2012 by the Free Software Foundation, Inc.
2
 
#
3
 
# This file is part of GNU Mailman.
4
 
#
5
 
# GNU Mailman is free software: you can redistribute it and/or modify it under
6
 
# the terms of the GNU General Public License as published by the Free
7
 
# Software Foundation, either version 3 of the License, or (at your option)
8
 
# any later version.
9
 
#
10
 
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13
 
# more details.
14
 
#
15
 
# You should have received a copy of the GNU General Public License along with
16
 
# GNU Mailman.  If not, see <http://www.gnu.org/licenses/>.
17
 
 
18
 
"""Mailman test layers."""
19
 
 
20
 
# XXX 2012-03-23 BAW: Layers really really suck.  For example, the
21
 
# test_owners_get_email() test requires that both the SMTPLayer and LMTPLayer
22
 
# be set up, but there's apparently no way to do that and make zope.testing
23
 
# happy.  This causes no tests failures, but it does cause errors at the end
24
 
# of the full test run.  For now, I'll ignore that, but I do want to
25
 
# eventually get rid of the zope.test* dependencies and use something like
26
 
# testresources or some such.
27
 
 
28
 
from __future__ import absolute_import, print_function, unicode_literals
29
 
 
30
 
__metaclass__ = type
31
 
__all__ = [
32
 
    'ConfigLayer',
33
 
    'LMTPLayer',
34
 
    'MockAndMonkeyLayer',
35
 
    'RESTLayer',
36
 
    'SMTPLayer',
37
 
    'is_testing',
38
 
    ]
39
 
 
40
 
 
41
 
import os
42
 
import sys
43
 
import shutil
44
 
import logging
45
 
import datetime
46
 
import tempfile
47
 
 
48
 
from base64 import b64encode
49
 
from lazr.config import as_boolean, as_timedelta
50
 
from pkg_resources import resource_string
51
 
from textwrap import dedent
52
 
from urllib2 import Request, URLError, urlopen
53
 
from zope.component import getUtility
54
 
 
55
 
from mailman.config import config
56
 
from mailman.core import initialize
57
 
from mailman.core.initialize import INHIBIT_CONFIG_FILE
58
 
from mailman.core.logging import get_handler
59
 
from mailman.database.transaction import transaction
60
 
from mailman.interfaces.domain import IDomainManager
61
 
from mailman.testing.helpers import (
62
 
    TestableMaster, get_lmtp_client, reset_the_world)
63
 
from mailman.testing.mta import ConnectionCountingController
64
 
from mailman.utilities.string import expand
65
 
 
66
 
 
67
 
TEST_TIMEOUT = datetime.timedelta(seconds=5)
68
 
NL = '\n'
69
 
 
70
 
 
71
 
 
72
 
class MockAndMonkeyLayer:
73
 
    """Layer for mocking and monkey patching for testing."""
74
 
 
75
 
    # Set this to True to enable predictable datetimes, uids, etc.
76
 
    testing_mode = False
77
 
 
78
 
    # A registration of all testing factories, for resetting between tests.
79
 
    _resets = []
80
 
 
81
 
    @classmethod
82
 
    def testTearDown(cls):
83
 
        for reset in cls._resets:
84
 
            reset()
85
 
 
86
 
    @classmethod
87
 
    def register_reset(cls, reset):
88
 
        cls._resets.append(reset)
89
 
 
90
 
 
91
 
 
92
 
class ConfigLayer(MockAndMonkeyLayer):
93
 
    """Layer for pushing and popping test configurations."""
94
 
 
95
 
    var_dir = None
96
 
    styles = None
97
 
 
98
 
    @classmethod
99
 
    def setUp(cls):
100
 
        # Set up the basic configuration stuff.  Turn off path creation until
101
 
        # we've pushed the testing config.
102
 
        config.create_paths = False
103
 
        initialize.initialize_1(INHIBIT_CONFIG_FILE)
104
 
        assert cls.var_dir is None, 'Layer already set up'
105
 
        # Calculate a temporary VAR_DIR directory so that run-time artifacts
106
 
        # of the tests won't tread on the installation's data.  This also
107
 
        # makes it easier to clean up after the tests are done, and insures
108
 
        # isolation of test suite runs.
109
 
        cls.var_dir = tempfile.mkdtemp()
110
 
        # We need a test configuration both for the foreground process and any
111
 
        # child processes that get spawned.  lazr.config would allow us to do
112
 
        # it all in a string that gets pushed, and we'll do that for the
113
 
        # foreground, but because we may be spawning processes (such as
114
 
        # runners) we'll need a file that we can specify to the with the -C
115
 
        # option.  Craft the full test configuration string here, push it, and
116
 
        # also write it out to a temp file for -C.
117
 
        test_config = dedent("""
118
 
        [mailman]
119
 
        layout: testing
120
 
        [paths.testing]
121
 
        var_dir: %s
122
 
        [devmode]
123
 
        testing: yes
124
 
        """ % cls.var_dir)
125
 
        # Read the testing config and push it.
126
 
        test_config += resource_string('mailman.testing', 'testing.cfg')
127
 
        config.create_paths = True
128
 
        config.push('test config', test_config)
129
 
        # Initialize everything else.
130
 
        initialize.initialize_2(testing=True)
131
 
        initialize.initialize_3()
132
 
        # When stderr debugging is enabled, subprocess root loggers should
133
 
        # also be more verbose.
134
 
        if cls.stderr:
135
 
            test_config += dedent("""
136
 
            [logging.root]
137
 
            propagate: yes
138
 
            level: debug
139
 
            """)
140
 
        # Enable log message propagation and reset the log paths so that the
141
 
        # doctests can check the output.
142
 
        for logger_config in config.logger_configs:
143
 
            sub_name = logger_config.name.split('.')[-1]
144
 
            if sub_name == 'root':
145
 
                continue
146
 
            logger_name = 'mailman.' + sub_name
147
 
            log = logging.getLogger(logger_name)
148
 
            log.propagate = True
149
 
            # Reopen the file to a new path that tests can get at.  Instead of
150
 
            # using the configuration file path though, use a path that's
151
 
            # specific to the logger so that tests can find expected output
152
 
            # more easily.
153
 
            path = os.path.join(config.LOG_DIR, sub_name)
154
 
            get_handler(sub_name).reopen(path)
155
 
            log.setLevel(logging.DEBUG)
156
 
            # If stderr debugging is enabled, make sure subprocesses are also
157
 
            # more verbose.
158
 
            if cls.stderr:
159
 
                test_config += expand(dedent("""
160
 
                [logging.$name]
161
 
                propagate: yes
162
 
                level: debug
163
 
                """), dict(name=sub_name, path=path))
164
 
        # zope.testing sets up logging before we get to our own initialization
165
 
        # function.  This messes with the root logger, so explicitly set it to
166
 
        # go to stderr.
167
 
        if cls.stderr:
168
 
            console = logging.StreamHandler(sys.stderr)
169
 
            formatter = logging.Formatter(config.logging.root.format,
170
 
                                          config.logging.root.datefmt)
171
 
            console.setFormatter(formatter)
172
 
            logging.getLogger().addHandler(console)
173
 
        # Write the configuration file for subprocesses and set up the config
174
 
        # object to pass that properly on the -C option.
175
 
        config_file = os.path.join(cls.var_dir, 'test.cfg')
176
 
        with open(config_file, 'w') as fp:
177
 
            fp.write(test_config)
178
 
            print(file=fp)
179
 
        config.filename = config_file
180
 
 
181
 
    @classmethod
182
 
    def tearDown(cls):
183
 
        assert cls.var_dir is not None, 'Layer not set up'
184
 
        config.pop('test config')
185
 
        shutil.rmtree(cls.var_dir)
186
 
        cls.var_dir = None
187
 
 
188
 
    @classmethod
189
 
    def testSetUp(cls):
190
 
        # Add an example domain.
191
 
        with transaction():
192
 
            getUtility(IDomainManager).add(
193
 
                'example.com', 'An example domain.',
194
 
                'http://lists.example.com', 'postmaster@example.com')
195
 
 
196
 
    @classmethod
197
 
    def testTearDown(cls):
198
 
        reset_the_world()
199
 
 
200
 
    # Flag to indicate that loggers should propagate to the console.
201
 
    stderr = False
202
 
 
203
 
    @classmethod
204
 
    def enable_stderr(cls):
205
 
        """Enable stderr logging if -e/--stderr is given.
206
 
 
207
 
        We used to hack our way into the zc.testing framework, but that was
208
 
        undocumented and way too fragile.  Well, this probably is too, but now
209
 
        we just scan sys.argv for -e/--stderr and enable logging if found.
210
 
        Then we remove the option from sys.argv.  This works because this
211
 
        method is called before zope.testrunner sees the options.
212
 
 
213
 
        As a bonus, we'll check an environment variable too.
214
 
        """
215
 
        if '-e' in sys.argv:
216
 
            cls.stderr = True
217
 
            sys.argv.remove('-e')
218
 
        if '--stderr' in sys.argv:
219
 
            cls.stderr = True
220
 
            sys.argv.remove('--stderr')
221
 
        if len(os.environ.get('MM_VERBOSE_TESTLOG', '').strip()) > 0:
222
 
            cls.stderr = True
223
 
 
224
 
    # The top of our source tree, for tests that care (e.g. hooks.txt).
225
 
    root_directory = None
226
 
 
227
 
    @classmethod
228
 
    def set_root_directory(cls, directory):
229
 
        """Set the directory at the root of our source tree.
230
 
 
231
 
        zc.recipe.testrunner runs from parts/test/working-directory, but
232
 
        that's actually changed over the life of the package.  Some tests
233
 
        care, e.g. because they need to find our built-out bin directory.
234
 
        Fortunately, buildout can give us this information.  See the
235
 
        `buildout.cfg` file for where this method is called.
236
 
        """
237
 
        cls.root_directory = directory
238
 
 
239
 
 
240
 
 
241
 
class SMTPLayer(ConfigLayer):
242
 
    """Layer for starting, stopping, and accessing a test SMTP server."""
243
 
 
244
 
    smtpd = None
245
 
 
246
 
    @classmethod
247
 
    def setUp(cls):
248
 
        assert cls.smtpd is None, 'Layer already set up'
249
 
        host = config.mta.smtp_host
250
 
        port = int(config.mta.smtp_port)
251
 
        cls.smtpd = ConnectionCountingController(host, port)
252
 
        cls.smtpd.start()
253
 
 
254
 
    @classmethod
255
 
    def tearDown(cls):
256
 
        assert cls.smtpd is not None, 'Layer not set up'
257
 
        cls.smtpd.clear()
258
 
        cls.smtpd.stop()
259
 
 
260
 
    @classmethod
261
 
    def testSetUp(cls):
262
 
        # Make sure we don't call our superclass's testSetUp(), otherwise the
263
 
        # example.com domain will get added twice.
264
 
        pass
265
 
 
266
 
    @classmethod
267
 
    def testTearDown(cls):
268
 
        cls.smtpd.reset()
269
 
        cls.smtpd.clear()
270
 
 
271
 
 
272
 
 
273
 
class LMTPLayer(ConfigLayer):
274
 
    """Layer for starting, stopping, and accessing a test LMTP server."""
275
 
 
276
 
    lmtpd = None
277
 
 
278
 
    @staticmethod
279
 
    def _wait_for_lmtp_server():
280
 
        get_lmtp_client(quiet=True)
281
 
 
282
 
    @classmethod
283
 
    def setUp(cls):
284
 
        assert cls.lmtpd is None, 'Layer already set up'
285
 
        cls.lmtpd = TestableMaster(cls._wait_for_lmtp_server)
286
 
        cls.lmtpd.start('lmtp')
287
 
 
288
 
    @classmethod
289
 
    def tearDown(cls):
290
 
        assert cls.lmtpd is not None, 'Layer not set up'
291
 
        cls.lmtpd.stop()
292
 
        cls.lmtpd = None
293
 
 
294
 
    @classmethod
295
 
    def testSetUp(cls):
296
 
        # Make sure we don't call our superclass's testSetUp(), otherwise the
297
 
        # example.com domain will get added twice.
298
 
        pass
299
 
 
300
 
 
301
 
 
302
 
class RESTLayer(SMTPLayer):
303
 
    """Layer for starting, stopping, and accessing the test REST layer."""
304
 
 
305
 
    server = None
306
 
 
307
 
    @staticmethod
308
 
    def _wait_for_rest_server():
309
 
        until = datetime.datetime.now() + as_timedelta(config.devmode.wait)
310
 
        while datetime.datetime.now() < until:
311
 
            try:
312
 
                request = Request('http://localhost:9001/3.0/system')
313
 
                basic_auth = '{0}:{1}'.format(config.webservice.admin_user,
314
 
                                              config.webservice.admin_pass)
315
 
                request.add_header('Authorization',
316
 
                                   'Basic ' + b64encode(basic_auth))
317
 
                fp = urlopen(request)
318
 
            except URLError:
319
 
                pass
320
 
            else:
321
 
                fp.close()
322
 
                break
323
 
        else:
324
 
            raise RuntimeError('REST server did not start up')
325
 
 
326
 
    @classmethod
327
 
    def setUp(cls):
328
 
        assert cls.server is None, 'Layer already set up'
329
 
        cls.server = TestableMaster(cls._wait_for_rest_server)
330
 
        cls.server.start('rest')
331
 
 
332
 
    @classmethod
333
 
    def tearDown(cls):
334
 
        assert cls.server is not None, 'Layer not set up'
335
 
        cls.server.stop()
336
 
        cls.server = None
337
 
 
338
 
 
339
 
 
340
 
def is_testing():
341
 
    """Return a 'testing' flag for use with the predictable factories.
342
 
 
343
 
    :return: True when in testing mode.
344
 
    :rtype: bool
345
 
    """
346
 
    return (MockAndMonkeyLayer.testing_mode or
347
 
            as_boolean(config.devmode.testing))