Merge lp:~allenap/launchpad/lp-app-longpoll into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 13375
Proposed branch: lp:~allenap/launchpad/lp-app-longpoll
Merge into: lp:launchpad
Diff against target: 702 lines (+617/-0)
12 files modified
lib/lp/app/configure.zcml (+1/-0)
lib/lp/app/longpoll/__init__.py (+47/-0)
lib/lp/app/longpoll/adapters/event.py (+47/-0)
lib/lp/app/longpoll/adapters/subscriber.py (+56/-0)
lib/lp/app/longpoll/adapters/tests/test_event.py (+84/-0)
lib/lp/app/longpoll/adapters/tests/test_subscriber.py (+134/-0)
lib/lp/app/longpoll/configure.zcml (+10/-0)
lib/lp/app/longpoll/interfaces.py (+51/-0)
lib/lp/app/longpoll/tests/__init__.py (+2/-0)
lib/lp/app/longpoll/tests/test_longpoll.py (+104/-0)
lib/lp/testing/fixture.py (+18/-0)
lib/lp/testing/tests/test_fixture.py (+63/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/lp-app-longpoll
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+66872@code.launchpad.net

Commit message

[r=gmb][no-qa] New lp.app.longpoll package.

Description of the change

New lp.app.longpoll package.

This provides a higher-level API on top of lp.services.messaging. It's
primary goal for now is to make it trivially easy to connect events in
the application to an interested page.

For example, an event in the application can be generated as simply
as:

    emit(an_object, "event_name", {...})

and a page being prepared can arrange to receive these event via
long-poll with a single line of code:

    subscribe(an_object, "event_name")

Behind the scenes a new, short-lived, queue will be created and
subscribed to the correct routing key for the event. The in-page JSON
cache will have the queue details added to it so that the client can
immediately long-poll for events.

This branch contains the server-side code. The client-side code is
being worked on by Raphael.

To support the emit/subscribe two-step a single adapter must be
registered. It must (multi-) adapt the object that issues events and
an event description to an ILongPollEvent. This branch contains a
suitable base-class, LongPollEvent. Suppose a Job emits events from
_set_status using emit(self, "status", status.name) the following
adapter would work:

    class JobLongPollEvent(LongPollEvent):

        adapts(IJob, Interface)
        implements(ILongPollEvent)

        @property
        def event_key(self):
            return generate_event_key(
                "job", self.source.id, self.event)

This ensures that there's a stable and consistent event naming scheme.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Gavin,

Nice to see all the work from last week landing. This is a good branch; a few nits but nothing huge. r=me pending the changes below.

[1]

396 +class TestModule(TestCase):

This seems like a bit of a generic name... Maybe TestSubscriberModule for clarity?

[2]

446 + source = Attribute(
447 + "The event source.")
448 +
449 + event = Attribute(
450 + "An object indicating the type of event.")

Minor stylistic nit: I don't think these need to be spread over two lines.

[3]

556 +class TestModule(TestCase):

Same as above. TestLongPollModule maybe?

[4]

560 + def test_subscribe(self):

576 + def test_emit(self):

681 + def test_register_and_unregister(self):

DMMT. These could do with some leading comments explaining the expected behaviour so that I don't have to read the tests.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/configure.zcml'
--- lib/lp/app/configure.zcml 2011-05-27 21:25:58 +0000
+++ lib/lp/app/configure.zcml 2011-07-05 13:35:30 +0000
@@ -10,6 +10,7 @@
10 xmlns:lp="http://namespaces.canonical.com/lp"10 xmlns:lp="http://namespaces.canonical.com/lp"
11 i18n_domain="launchpad">11 i18n_domain="launchpad">
12 <include package=".browser"/>12 <include package=".browser"/>
13 <include package=".longpoll" />
13 <include package="lp.app.validators" />14 <include package="lp.app.validators" />
14 <include package="lp.app.widgets" />15 <include package="lp.app.widgets" />
1516
1617
=== added directory 'lib/lp/app/longpoll'
=== added file 'lib/lp/app/longpoll/__init__.py'
--- lib/lp/app/longpoll/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/__init__.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,47 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll infrastructure."""
5
6__metaclass__ = type
7__all__ = [
8 "emit",
9 "subscribe",
10 ]
11
12from .interfaces import (
13 ILongPollEvent,
14 ILongPollSubscriber,
15 )
16from lazr.restful.utils import get_current_browser_request
17from zope.component import getMultiAdapter
18
19
20def subscribe(target, event, request=None):
21 """Convenience method to subscribe the current request.
22
23 :param target: Something that can be adapted to `ILongPollEvent`.
24 :param event: The name of the event to subscribe to.
25 :param request: The request for which to get an `ILongPollSubscriber`. It
26 a request is not specified the currently active request is used.
27 :return: The `ILongPollEvent` that has been subscribed to.
28 """
29 event = getMultiAdapter((target, event), ILongPollEvent)
30 if request is None:
31 request = get_current_browser_request()
32 subscriber = ILongPollSubscriber(request)
33 subscriber.subscribe(event)
34 return event
35
36
37def emit(source, event, data):
38 """Convenience method to emit a message for an event.
39
40 :param source: Something, along with `event`, that can be adapted to
41 `ILongPollEvent`.
42 :param event: A name/key of the event that is emitted.
43 :return: The `ILongPollEvent` that has been emitted.
44 """
45 event = getMultiAdapter((source, event), ILongPollEvent)
46 event.emit(data)
47 return event
048
=== added directory 'lib/lp/app/longpoll/adapters'
=== added file 'lib/lp/app/longpoll/adapters/__init__.py'
=== added file 'lib/lp/app/longpoll/adapters/event.py'
--- lib/lp/app/longpoll/adapters/event.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/adapters/event.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,47 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long poll adapters."""
5
6__metaclass__ = type
7__all__ = [
8 "generate_event_key",
9 "LongPollEvent",
10 ]
11
12from lp.services.messaging.queue import RabbitRoutingKey
13
14
15def generate_event_key(*components):
16 """Generate a suitable event name."""
17 if len(components) == 0:
18 raise AssertionError(
19 "Event keys must contain at least one component.")
20 return "longpoll.event.%s" % ".".join(
21 str(component) for component in components)
22
23
24class LongPollEvent:
25 """Base-class for event adapters.
26
27 Sub-classes need to declare something along the lines of:
28
29 adapts(Interface, Interface)
30 implements(ILongPollEvent)
31
32 """
33
34 def __init__(self, source, event):
35 self.source = source
36 self.event = event
37
38 @property
39 def event_key(self):
40 """See `ILongPollEvent`."""
41 raise NotImplementedError(self.__class__.event_key)
42
43 def emit(self, data):
44 """See `ILongPollEvent`."""
45 payload = {"event_key": self.event_key, "event_data": data}
46 routing_key = RabbitRoutingKey(self.event_key)
47 routing_key.send(payload)
048
=== added file 'lib/lp/app/longpoll/adapters/subscriber.py'
--- lib/lp/app/longpoll/adapters/subscriber.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/adapters/subscriber.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,56 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long poll adapters."""
5
6__metaclass__ = type
7__all__ = [
8 "generate_subscribe_key",
9 "LongPollSubscriber",
10 ]
11
12from uuid import uuid4
13
14from lazr.restful.interfaces import IJSONRequestCache
15from zope.component import adapts
16from zope.interface import implements
17from zope.publisher.interfaces import IApplicationRequest
18
19from lp.app.longpoll.interfaces import ILongPollSubscriber
20from lp.services.messaging.queue import (
21 RabbitQueue,
22 RabbitRoutingKey,
23 )
24
25
26def generate_subscribe_key():
27 """Generate a suitable new, unique, subscribe key."""
28 return "longpoll.subscribe.%s" % uuid4()
29
30
31class LongPollSubscriber:
32
33 adapts(IApplicationRequest)
34 implements(ILongPollSubscriber)
35
36 def __init__(self, request):
37 self.request = request
38
39 @property
40 def subscribe_key(self):
41 objects = IJSONRequestCache(self.request).objects
42 if "longpoll" in objects:
43 return objects["longpoll"]["key"]
44 return None
45
46 def subscribe(self, event):
47 cache = IJSONRequestCache(self.request)
48 if "longpoll" not in cache.objects:
49 cache.objects["longpoll"] = {
50 "key": generate_subscribe_key(),
51 "subscriptions": [],
52 }
53 subscribe_queue = RabbitQueue(self.subscribe_key)
54 routing_key = RabbitRoutingKey(event.event_key)
55 routing_key.associateConsumer(subscribe_queue)
56 cache.objects["longpoll"]["subscriptions"].append(event.event_key)
057
=== added directory 'lib/lp/app/longpoll/adapters/tests'
=== added file 'lib/lp/app/longpoll/adapters/tests/__init__.py'
=== added file 'lib/lp/app/longpoll/adapters/tests/test_event.py'
--- lib/lp/app/longpoll/adapters/tests/test_event.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/adapters/tests/test_event.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,84 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll event adapter tests."""
5
6__metaclass__ = type
7
8from zope.interface import implements
9
10from canonical.testing.layers import (
11 BaseLayer,
12 LaunchpadFunctionalLayer,
13 )
14from lp.app.longpoll.adapters.event import (
15 generate_event_key,
16 LongPollEvent,
17 )
18from lp.app.longpoll.interfaces import ILongPollEvent
19from lp.services.messaging.queue import RabbitMessageBase
20from lp.testing import TestCase
21from lp.testing.matchers import Contains
22
23
24class FakeEvent(LongPollEvent):
25
26 implements(ILongPollEvent)
27
28 @property
29 def event_key(self):
30 return "event-key-%s-%s" % (self.source, self.event)
31
32
33class TestLongPollEvent(TestCase):
34
35 layer = LaunchpadFunctionalLayer
36
37 def test_interface(self):
38 event = FakeEvent("source", "event")
39 self.assertProvides(event, ILongPollEvent)
40
41 def test_event_key(self):
42 # event_key is not implemented in LongPollEvent; subclasses must
43 # provide it.
44 event = LongPollEvent("source", "event")
45 self.assertRaises(NotImplementedError, getattr, event, "event_key")
46
47 def test_emit(self):
48 # LongPollEvent.emit() sends the given data to `event_key`.
49 event = FakeEvent("source", "event")
50 event_data = {"hello": 1234}
51 event.emit(event_data)
52 expected_message = {
53 "event_key": event.event_key,
54 "event_data": event_data,
55 }
56 pending_messages = [
57 message for (call, message) in
58 RabbitMessageBase.class_locals.messages]
59 self.assertThat(pending_messages, Contains(expected_message))
60
61
62class TestFunctions(TestCase):
63
64 layer = BaseLayer
65
66 def test_generate_event_key_no_components(self):
67 self.assertRaises(
68 AssertionError, generate_event_key)
69
70 def test_generate_event_key(self):
71 self.assertEqual(
72 "longpoll.event.event-name",
73 generate_event_key("event-name"))
74 self.assertEqual(
75 "longpoll.event.source-name.event-name",
76 generate_event_key("source-name", "event-name"))
77 self.assertEqual(
78 "longpoll.event.type-name.source-name.event-name",
79 generate_event_key("type-name", "source-name", "event-name"))
80
81 def test_generate_event_key_stringifies_components(self):
82 self.assertEqual(
83 "longpoll.event.job.1234.COMPLETED",
84 generate_event_key("job", 1234, "COMPLETED"))
085
=== added file 'lib/lp/app/longpoll/adapters/tests/test_subscriber.py'
--- lib/lp/app/longpoll/adapters/tests/test_subscriber.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/adapters/tests/test_subscriber.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,134 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll subscriber adapter tests."""
5
6__metaclass__ = type
7
8from itertools import count
9
10from lazr.restful.interfaces import IJSONRequestCache
11from testtools.matchers import (
12 Not,
13 StartsWith,
14 )
15from zope.interface import implements
16
17from canonical.launchpad.webapp.servers import LaunchpadTestRequest
18from canonical.testing.layers import (
19 BaseLayer,
20 LaunchpadFunctionalLayer,
21 )
22from lp.app.longpoll.adapters.subscriber import (
23 generate_subscribe_key,
24 LongPollSubscriber,
25 )
26from lp.app.longpoll.interfaces import (
27 ILongPollEvent,
28 ILongPollSubscriber,
29 )
30from lp.services.messaging.queue import (
31 RabbitQueue,
32 RabbitRoutingKey,
33 )
34from lp.testing import TestCase
35from lp.testing.matchers import Contains
36
37
38class FakeEvent:
39
40 implements(ILongPollEvent)
41
42 event_key_indexes = count(1)
43
44 def __init__(self):
45 self.event_key = "event-key-%d" % next(self.event_key_indexes)
46
47
48class TestLongPollSubscriber(TestCase):
49
50 layer = LaunchpadFunctionalLayer
51
52 def test_interface(self):
53 request = LaunchpadTestRequest()
54 subscriber = LongPollSubscriber(request)
55 self.assertProvides(subscriber, ILongPollSubscriber)
56
57 def test_subscribe_key(self):
58 request = LaunchpadTestRequest()
59 subscriber = LongPollSubscriber(request)
60 # A subscribe key is not generated yet.
61 self.assertIs(subscriber.subscribe_key, None)
62 # It it only generated on the first subscription.
63 subscriber.subscribe(FakeEvent())
64 subscribe_key = subscriber.subscribe_key
65 self.assertIsInstance(subscribe_key, str)
66 self.assertNotEqual(0, len(subscribe_key))
67 # It remains the same for later subscriptions.
68 subscriber.subscribe(FakeEvent())
69 self.assertEqual(subscribe_key, subscriber.subscribe_key)
70
71 def test_adapter(self):
72 request = LaunchpadTestRequest()
73 subscriber = ILongPollSubscriber(request)
74 self.assertIsInstance(subscriber, LongPollSubscriber)
75 # A difference subscriber is returned on subsequent adaptions, but it
76 # has the same subscribe_key.
77 subscriber2 = ILongPollSubscriber(request)
78 self.assertIsNot(subscriber, subscriber2)
79 self.assertEqual(subscriber.subscribe_key, subscriber2.subscribe_key)
80
81 def test_subscribe_queue(self):
82 # LongPollSubscriber.subscribe() creates a new queue with a new unique
83 # name that is bound to the event's event_key.
84 request = LaunchpadTestRequest()
85 event = FakeEvent()
86 subscriber = ILongPollSubscriber(request)
87 subscriber.subscribe(event)
88 message = '{"hello": 1234}'
89 routing_key = RabbitRoutingKey(event.event_key)
90 routing_key.send_now(message)
91 subscribe_queue = RabbitQueue(subscriber.subscribe_key)
92 self.assertEqual(
93 message, subscribe_queue.receive(timeout=5))
94
95 def test_json_cache_not_populated_on_init(self):
96 # LongPollSubscriber does not put the name of the new queue into the
97 # JSON cache.
98 request = LaunchpadTestRequest()
99 cache = IJSONRequestCache(request)
100 self.assertThat(cache.objects, Not(Contains("longpoll")))
101 ILongPollSubscriber(request)
102 self.assertThat(cache.objects, Not(Contains("longpoll")))
103
104 def test_json_cache_populated_on_subscribe(self):
105 # To aid with debugging the event_key of subscriptions are added to
106 # the JSON cache.
107 request = LaunchpadTestRequest()
108 cache = IJSONRequestCache(request)
109 event1 = FakeEvent()
110 ILongPollSubscriber(request).subscribe(event1) # Side-effects!
111 self.assertThat(cache.objects, Contains("longpoll"))
112 self.assertThat(cache.objects["longpoll"], Contains("key"))
113 self.assertThat(cache.objects["longpoll"], Contains("subscriptions"))
114 self.assertEqual(
115 [event1.event_key],
116 cache.objects["longpoll"]["subscriptions"])
117 # More events can be subscribed.
118 event2 = FakeEvent()
119 ILongPollSubscriber(request).subscribe(event2)
120 self.assertEqual(
121 [event1.event_key, event2.event_key],
122 cache.objects["longpoll"]["subscriptions"])
123
124
125class TestFunctions(TestCase):
126
127 layer = BaseLayer
128
129 def test_generate_subscribe_key(self):
130 subscribe_key = generate_subscribe_key()
131 expected_prefix = "longpoll.subscribe."
132 self.assertThat(subscribe_key, StartsWith(expected_prefix))
133 # The key contains a 36 character UUID.
134 self.assertEqual(len(expected_prefix) + 36, len(subscribe_key))
0135
=== added file 'lib/lp/app/longpoll/configure.zcml'
--- lib/lp/app/longpoll/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/configure.zcml 2011-07-05 13:35:30 +0000
@@ -0,0 +1,10 @@
1<!-- Copyright 2011 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4<configure
5 xmlns="http://namespaces.zope.org/zope"
6 xmlns:browser="http://namespaces.zope.org/browser"
7 xmlns:i18n="http://namespaces.zope.org/i18n"
8 i18n_domain="launchpad">
9 <adapter factory=".adapters.subscriber.LongPollSubscriber" />
10</configure>
011
=== added file 'lib/lp/app/longpoll/interfaces.py'
--- lib/lp/app/longpoll/interfaces.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/interfaces.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,51 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll infrastructure interfaces."""
5
6__metaclass__ = type
7__all__ = [
8 "ILongPollEvent",
9 "ILongPollSubscriber",
10 ]
11
12
13from zope.interface import (
14 Attribute,
15 Interface,
16 )
17
18
19class ILongPollEvent(Interface):
20
21 source = Attribute("The event source.")
22
23 event = Attribute("An object indicating the type of event.")
24
25 event_key = Attribute(
26 "The key with which events will be emitted. Should be predictable "
27 "and stable.")
28
29 def emit(data):
30 """Emit the given data to `event_key`.
31
32 The data will be wrapped up into a `dict` with the keys `event_key`
33 and `event_data`, where `event_key` is a copy of `self.event_key` and
34 `event_data` is the `data` argument.
35
36 :param data: Any data structure that can be dumped as JSON.
37 """
38
39
40class ILongPollSubscriber(Interface):
41
42 subscribe_key = Attribute(
43 "The key which the subscriber must know in order to be able "
44 "to long-poll for subscribed events. Should be infeasible to "
45 "guess, a UUID for example.")
46
47 def subscribe(event):
48 """Subscribe to the given event.
49
50 :type event: ILongPollEvent
51 """
052
=== added directory 'lib/lp/app/longpoll/tests'
=== added file 'lib/lp/app/longpoll/tests/__init__.py'
--- lib/lp/app/longpoll/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/tests/__init__.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,2 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
03
=== added file 'lib/lp/app/longpoll/tests/test_longpoll.py'
--- lib/lp/app/longpoll/tests/test_longpoll.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/longpoll/tests/test_longpoll.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,104 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for lp.app.longpoll."""
5
6__metaclass__ = type
7
8from zope.component import adapts
9from zope.interface import (
10 Attribute,
11 implements,
12 Interface,
13 )
14
15from canonical.launchpad.webapp.servers import LaunchpadTestRequest
16from canonical.testing.layers import LaunchpadFunctionalLayer
17from lp.app.longpoll import (
18 emit,
19 subscribe,
20 )
21from lp.app.longpoll.interfaces import (
22 ILongPollEvent,
23 ILongPollSubscriber,
24 )
25from lp.services.messaging.queue import (
26 RabbitQueue,
27 RabbitRoutingKey,
28 )
29from lp.testing import TestCase
30from lp.testing.fixture import ZopeAdapterFixture
31
32
33class IFakeObject(Interface):
34
35 ident = Attribute("ident")
36
37
38class FakeObject:
39
40 implements(IFakeObject)
41
42 def __init__(self, ident):
43 self.ident = ident
44
45
46class FakeEvent:
47
48 adapts(IFakeObject, Interface)
49 implements(ILongPollEvent)
50
51 def __init__(self, source, event):
52 self.source = source
53 self.event = event
54
55 @property
56 def event_key(self):
57 return "event-key-%s-%s" % (
58 self.source.ident, self.event)
59
60 def emit(self, data):
61 # Don't cargo-cult this; see .adapters.event.LongPollEvent instead.
62 RabbitRoutingKey(self.event_key).send_now(data)
63
64
65class TestFunctions(TestCase):
66
67 layer = LaunchpadFunctionalLayer
68
69 def test_subscribe(self):
70 # subscribe() gets the ILongPollEvent for the given (target, event)
71 # and the ILongPollSubscriber for the given request (or the current
72 # request is discovered). It subscribes the latter to the event, then
73 # returns the event.
74 request = LaunchpadTestRequest()
75 an_object = FakeObject(12345)
76 with ZopeAdapterFixture(FakeEvent):
77 event = subscribe(an_object, "foo", request=request)
78 self.assertIsInstance(event, FakeEvent)
79 self.assertEqual("event-key-12345-foo", event.event_key)
80 # Emitting an event-key-12345-foo event will put something on the
81 # subscriber's queue.
82 event_data = {"1234": 5678}
83 event.emit(event_data)
84 subscriber = ILongPollSubscriber(request)
85 subscribe_queue = RabbitQueue(subscriber.subscribe_key)
86 message = subscribe_queue.receive(timeout=5)
87 self.assertEqual(event_data, message)
88
89 def test_emit(self):
90 # subscribe() gets the ILongPollEvent for the given (target, event)
91 # and passes the given data to its emit() method. It then returns the
92 # event.
93 an_object = FakeObject(12345)
94 with ZopeAdapterFixture(FakeEvent):
95 event = emit(an_object, "bar", {})
96 routing_key = RabbitRoutingKey(event.event_key)
97 subscribe_queue = RabbitQueue("whatever")
98 routing_key.associateConsumer(subscribe_queue)
99 # Emit the event again; the subscribe queue was not associated
100 # with the event before now.
101 event_data = {"8765": 4321}
102 event = emit(an_object, "bar", event_data)
103 message = subscribe_queue.receive(timeout=5)
104 self.assertEqual(event_data, message)
0105
=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py 2011-06-10 02:30:43 +0000
+++ lib/lp/testing/fixture.py 2011-07-05 13:35:30 +0000
@@ -5,7 +5,9 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'ZopeAdapterFixture',
8 'ZopeEventHandlerFixture',9 'ZopeEventHandlerFixture',
10 'ZopeViewReplacementFixture',
9 ]11 ]
1012
11from fixtures import Fixture13from fixtures import Fixture
@@ -22,6 +24,22 @@
22 )24 )
2325
2426
27class ZopeAdapterFixture(Fixture):
28 """A fixture to register and unregister an adapter."""
29
30 def __init__(self, *args, **kwargs):
31 self._args, self._kwargs = args, kwargs
32
33 def setUp(self):
34 super(ZopeAdapterFixture, self).setUp()
35 site_manager = getGlobalSiteManager()
36 site_manager.registerAdapter(
37 *self._args, **self._kwargs)
38 self.addCleanup(
39 site_manager.unregisterAdapter,
40 *self._args, **self._kwargs)
41
42
25class ZopeEventHandlerFixture(Fixture):43class ZopeEventHandlerFixture(Fixture):
26 """A fixture that provides and then unprovides a Zope event handler."""44 """A fixture that provides and then unprovides a Zope event handler."""
2745
2846
=== added file 'lib/lp/testing/tests/test_fixture.py'
--- lib/lp/testing/tests/test_fixture.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testing/tests/test_fixture.py 2011-07-05 13:35:30 +0000
@@ -0,0 +1,63 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for lp.testing.fixture."""
5
6__metaclass__ = type
7
8from zope.component import (
9 adapts,
10 queryAdapter,
11 )
12from zope.interface import (
13 implements,
14 Interface,
15 )
16
17from canonical.testing.layers import BaseLayer
18from lp.testing import TestCase
19from lp.testing.fixture import ZopeAdapterFixture
20
21
22class IFoo(Interface):
23 pass
24
25
26class IBar(Interface):
27 pass
28
29
30class Foo:
31 implements(IFoo)
32
33
34class Bar:
35 implements(IBar)
36
37
38class FooToBar:
39
40 adapts(IFoo)
41 implements(IBar)
42
43 def __init__(self, foo):
44 self.foo = foo
45
46
47class TestZopeAdapterFixture(TestCase):
48
49 layer = BaseLayer
50
51 def test_register_and_unregister(self):
52 # Entering ZopeAdapterFixture's context registers the given adapter,
53 # and exiting the context unregisters the adapter again.
54 context = Foo()
55 # No adapter from Foo to Bar is registered.
56 self.assertIs(None, queryAdapter(context, IBar))
57 with ZopeAdapterFixture(FooToBar):
58 # Now there is an adapter from Foo to Bar.
59 adapter = queryAdapter(context, IBar)
60 self.assertIsNot(None, adapter)
61 self.assertIsInstance(adapter, FooToBar)
62 # The adapter is no longer registered.
63 self.assertIs(None, queryAdapter(context, IBar))