~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/testing/fixture.py

  • Committer: Curtis Hovey
  • Date: 2011-08-21 14:21:06 UTC
  • mto: This revision was merged to the branch mainline in revision 13745.
  • Revision ID: curtis.hovey@canonical.com-20110821142106-x93hajd6iguma8gx
Update test that was enforcing bad grammar.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
1
# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
3
 
4
4
"""Launchpad test fixtures that have no better home."""
5
5
 
6
6
__metaclass__ = type
7
7
__all__ = [
8
 
    'CaptureOops',
9
 
    'DemoMode',
10
 
    'PGBouncerFixture',
11
 
    'Urllib2Fixture',
 
8
    'RabbitServer',
12
9
    'ZopeAdapterFixture',
13
10
    'ZopeEventHandlerFixture',
14
11
    'ZopeViewReplacementFixture',
15
12
    ]
16
13
 
17
 
from ConfigParser import SafeConfigParser
18
 
import os.path
19
 
import time
 
14
from textwrap import dedent
20
15
 
21
 
import amqplib.client_0_8 as amqp
22
 
from fixtures import (
23
 
    EnvironmentVariableFixture,
24
 
    Fixture,
25
 
    )
26
 
from lazr.restful.utils import get_current_browser_request
27
 
import oops
28
 
import oops_amqp
29
 
import pgbouncer.fixture
30
 
from wsgi_intercept import (
31
 
    add_wsgi_intercept,
32
 
    remove_wsgi_intercept,
33
 
    )
34
 
from wsgi_intercept.urllib2_intercept import (
35
 
    install_opener,
36
 
    uninstall_opener,
37
 
    )
 
16
from fixtures import Fixture
 
17
import rabbitfixture.server
38
18
from zope.component import (
39
 
    adapter,
40
19
    getGlobalSiteManager,
41
20
    provideHandler,
42
21
    )
48
27
    undefineChecker,
49
28
    )
50
29
 
51
 
from canonical.config import config
52
 
from canonical.launchpad import webapp
53
 
from canonical.launchpad.webapp.errorlog import ErrorReportEvent
54
 
from lp.services.messaging.interfaces import MessagingUnavailable
55
 
from lp.services.messaging.rabbit import connect
56
 
from lp.services.timeline.requesttimeline import get_request_timeline
57
 
 
58
 
 
59
 
class PGBouncerFixture(pgbouncer.fixture.PGBouncerFixture):
60
 
    """Inserts a controllable pgbouncer instance in front of PostgreSQL.
61
 
 
62
 
    The pgbouncer proxy can be shutdown and restarted at will, simulating
63
 
    database outages and fastdowntime deployments.
 
30
 
 
31
class RabbitServer(rabbitfixture.server.RabbitServer):
 
32
    """A RabbitMQ server fixture with Launchpad-specific config.
 
33
 
 
34
    :ivar service_config: A snippet of .ini that describes the `rabbitmq`
 
35
        configuration.
64
36
    """
65
37
 
66
 
    def __init__(self):
67
 
        super(PGBouncerFixture, self).__init__()
68
 
 
69
 
        # Known databases
70
 
        from canonical.testing.layers import DatabaseLayer
71
 
        dbnames = [
72
 
            DatabaseLayer._db_fixture.dbname,
73
 
            DatabaseLayer._db_template_fixture.dbname,
74
 
            'session_ftest',
75
 
            'launchpad_empty',
76
 
            ]
77
 
        for dbname in dbnames:
78
 
            self.databases[dbname] = 'dbname=%s port=5432 host=localhost' % (
79
 
                dbname,)
80
 
 
81
 
        # Known users, pulled from security.cfg
82
 
        security_cfg_path = os.path.join(
83
 
            config.root, 'database', 'schema', 'security.cfg')
84
 
        security_cfg_config = SafeConfigParser({})
85
 
        security_cfg_config.read([security_cfg_path])
86
 
        for section_name in security_cfg_config.sections():
87
 
            self.users[section_name] = 'trusted'
88
 
            self.users[section_name + '_ro'] = 'trusted'
89
 
        self.users[os.environ['USER']] = 'trusted'
90
 
        self.users['pgbouncer'] = 'trusted'
91
 
 
92
 
        # Administrative access is useful for debugging.
93
 
        self.admin_users = ['launchpad', 'pgbouncer', os.environ['USER']]
94
 
 
95
38
    def setUp(self):
96
 
        super(PGBouncerFixture, self).setUp()
97
 
 
98
 
        # reconnect_store cleanup added first so it is run last, after
99
 
        # the environment variables have been reset.
100
 
        self.addCleanup(self._maybe_reconnect_stores)
101
 
 
102
 
        # Abuse the PGPORT environment variable to get things connecting
103
 
        # via pgbouncer. Otherwise, we would need to temporarily
104
 
        # overwrite the database connection strings in the config.
105
 
        self.useFixture(EnvironmentVariableFixture('PGPORT', str(self.port)))
106
 
 
107
 
        # Reset database connections so they go through pgbouncer.
108
 
        self._maybe_reconnect_stores()
109
 
 
110
 
    def _maybe_reconnect_stores(self):
111
 
        """Force Storm Stores to reconnect if they are registered.
112
 
 
113
 
        This is a noop if the Component Architecture is not loaded,
114
 
        as we are using a test layer that doesn't provide database
115
 
        connections.
116
 
        """
117
 
        from canonical.testing.layers import (
118
 
            reconnect_stores,
119
 
            is_ca_available,
120
 
            )
121
 
        if is_ca_available():
122
 
            reconnect_stores()
 
39
        super(RabbitServer, self).setUp()
 
40
        self.config.service_config = dedent("""\
 
41
            [rabbitmq]
 
42
            host: localhost:%d
 
43
            userid: guest
 
44
            password: guest
 
45
            virtual_host: /
 
46
            """ % self.config.port)
123
47
 
124
48
 
125
49
class ZopeAdapterFixture(Fixture):
190
114
        # can add more flexibility then.
191
115
        defineChecker(self.replacement, self.checker)
192
116
 
193
 
        self.addCleanup(
194
 
            undefineChecker, self.replacement)
195
 
        self.addCleanup(
196
 
            self.gsm.adapters.register,
197
 
            (self.context_interface, self.request_interface),
198
 
            Interface,
199
 
            self.name, self.original)
200
 
 
201
 
 
202
 
class ZopeUtilityFixture(Fixture):
203
 
    """A fixture that temporarily registers a different utility."""
204
 
 
205
 
    def __init__(self, component, intf, name):
206
 
        """Construct a new fixture.
207
 
 
208
 
        :param component: An instance of a class that provides this
209
 
            interface.
210
 
        :param intf: The Zope interface class to register, eg
211
 
            IMailDelivery.
212
 
        :param name: A string name to match.
213
 
        """
214
 
        self.component = component
215
 
        self.name = name
216
 
        self.intf = intf
217
 
 
218
 
    def setUp(self):
219
 
        super(ZopeUtilityFixture, self).setUp()
220
 
        gsm = getGlobalSiteManager()
221
 
        gsm.registerUtility(self.component, self.intf, self.name)
222
 
        self.addCleanup(
223
 
            gsm.unregisterUtility,
224
 
            self.component, self.intf, self.name)
225
 
 
226
 
 
227
 
class Urllib2Fixture(Fixture):
228
 
    """Let tests use urllib to connect to an in-process Launchpad.
229
 
 
230
 
    Initially this only supports connecting to launchpad.dev because
231
 
    that is all that is needed.  Later work could connect all
232
 
    sub-hosts (e.g. bugs.launchpad.dev)."""
233
 
 
234
 
    def setUp(self):
235
 
        # Work around circular import.
236
 
        from canonical.testing.layers import wsgi_application
237
 
        super(Urllib2Fixture, self).setUp()
238
 
        add_wsgi_intercept('launchpad.dev', 80, lambda: wsgi_application)
239
 
        self.addCleanup(remove_wsgi_intercept, 'launchpad.dev', 80)
240
 
        install_opener()
241
 
        self.addCleanup(uninstall_opener)
242
 
 
243
 
 
244
 
class CaptureOops(Fixture):
245
 
    """Capture OOPSes notified via zope event notification.
246
 
 
247
 
    :ivar oopses: A list of the oops objects raised while the fixture is
248
 
        setup.
249
 
    :ivar oops_ids: A set of observed oops ids. Used to de-dup reports
250
 
        received over AMQP.
251
 
    """
252
 
 
253
 
    AMQP_SENTINEL = "STOP NOW"
254
 
 
255
 
    def setUp(self):
256
 
        super(CaptureOops, self).setUp()
257
 
        self.oopses = []
258
 
        self.oops_ids = set()
259
 
        self.useFixture(ZopeEventHandlerFixture(self._recordOops))
260
 
        try:
261
 
            self.connection = connect()
262
 
        except MessagingUnavailable:
263
 
            self.channel = None
264
 
        else:
265
 
            self.addCleanup(self.connection.close)
266
 
            self.channel = self.connection.channel()
267
 
            self.addCleanup(self.channel.close)
268
 
            self.oops_config = oops.Config()
269
 
            self.oops_config.publishers.append(self._add_oops)
270
 
            self.setUpQueue()
271
 
 
272
 
    def setUpQueue(self):
273
 
        """Sets up the queue to be used to receive reports.
274
 
 
275
 
        The queue is autodelete which means we can only use it once: after
276
 
        that it will be automatically nuked and must be recreated.
277
 
        """
278
 
        self.queue_name, _, _ = self.channel.queue_declare(
279
 
            durable=True, auto_delete=True)
280
 
        # In production the exchange already exists and is durable, but
281
 
        # here we make it just-in-time, and tell it to go when the test
282
 
        # fixture goes.
283
 
        self.channel.exchange_declare(config.error_reports.error_exchange,
284
 
            "fanout", durable=True, auto_delete=True)
285
 
        self.channel.queue_bind(
286
 
            self.queue_name, config.error_reports.error_exchange)
287
 
 
288
 
    def _add_oops(self, report):
289
 
        """Add an oops if it isn't already recorded.
290
 
 
291
 
        This is called from both amqp and in-appserver situations.
292
 
        """
293
 
        if report['id'] not in self.oops_ids:
294
 
            self.oopses.append(report)
295
 
            self.oops_ids.add(report['id'])
296
 
 
297
 
    @adapter(ErrorReportEvent)
298
 
    def _recordOops(self, event):
299
 
        """Callback from zope publishing to publish oopses."""
300
 
        self._add_oops(event.object)
301
 
 
302
 
    def sync(self):
303
 
        """Sync the in-memory list of OOPS with the external OOPS source."""
304
 
        if not self.channel:
305
 
            return
306
 
        # Send ourselves a message: when we receive this, we've processed all
307
 
        # oopses created before sync() was invoked.
308
 
        message = amqp.Message(self.AMQP_SENTINEL)
309
 
        # Match what oops publishing does
310
 
        message.properties["delivery_mode"] = 2
311
 
        # Publish the message via a new channel (otherwise rabbit
312
 
        # shortcircuits it straight back to us, apparently).
313
 
        connection = connect()
314
 
        try:
315
 
            channel = connection.channel()
316
 
            try:
317
 
                channel.basic_publish(
318
 
                    message, config.error_reports.error_exchange,
319
 
                    config.error_reports.error_queue_key)
320
 
            finally:
321
 
                channel.close()
322
 
        finally:
323
 
            connection.close()
324
 
        receiver = oops_amqp.Receiver(
325
 
            self.oops_config, connect, self.queue_name)
326
 
        receiver.sentinel = self.AMQP_SENTINEL
327
 
        try:
328
 
            receiver.run_forever()
329
 
        finally:
330
 
            # Ensure we leave the queue ready to roll, or later calls to
331
 
            # sync() will fail.
332
 
            self.setUpQueue()
333
 
 
334
 
 
335
 
class CaptureTimeline(Fixture):
336
 
    """Record and return the timeline.
337
 
 
338
 
    This won't work well (yet) for code that starts new requests as they will
339
 
    reset the timeline.
340
 
    """
341
 
 
342
 
    def setUp(self):
343
 
        Fixture.setUp(self)
344
 
        webapp.adapter.set_request_started(time.time())
345
 
        self.timeline = get_request_timeline(
346
 
            get_current_browser_request())
347
 
        self.addCleanup(webapp.adapter.clear_request_started)
348
 
 
349
 
 
350
 
class DemoMode(Fixture):
351
 
    """Run with an is_demo configuration.
352
 
 
353
 
    This changes the page styling, feature flag permissions, and perhaps
354
 
    other things.
355
 
    """
356
 
 
357
 
    def setUp(self):
358
 
        Fixture.setUp(self)
359
 
        config.push('demo-fixture', '''
360
 
[launchpad]
361
 
is_demo: true
362
 
site_message = This is a demo site mmk. \
363
 
<a href="http://example.com">File a bug</a>.
364
 
            ''')
365
 
        self.addCleanup(lambda: config.pop('demo-fixture'))
 
117
    def tearDown(self):
 
118
        super(ZopeViewReplacementFixture, self).tearDown()
 
119
        undefineChecker(self.replacement)
 
120
        self.gsm.adapters.register(
 
121
            (self.context_interface, self.request_interface), Interface,
 
122
             self.name, self.original)