Merge lp:~allenap/launchpad/add-rabbit-service-bug-806160 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 13969
Proposed branch: lp:~allenap/launchpad/add-rabbit-service-bug-806160
Merge into: lp:launchpad
Diff against target: 423 lines (+133/-66)
11 files modified
Makefile (+2/-2)
configs/development/launchpad-lazr.conf (+7/-0)
configs/testrunner/launchpad-lazr.conf (+7/-0)
lib/canonical/config/schema-lazr.conf (+16/-9)
lib/canonical/launchpad/scripts/runlaunchpad.py (+25/-7)
lib/canonical/launchpad/scripts/tests/test_runlaunchpad.py (+4/-0)
lib/canonical/testing/layers.py (+1/-1)
lib/lp/services/rabbit/server.py (+31/-0)
lib/lp/services/rabbit/tests/test_server.py (+40/-0)
lib/lp/testing/fixture.py (+0/-21)
lib/lp/testing/tests/test_fixture.py (+0/-26)
To merge this branch: bzr merge lp:~allenap/launchpad/add-rabbit-service-bug-806160
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+75195@code.launchpad.net

Commit message

[r=jtv][bug=806160] Start up RabbitMQ on a known port when using make run and make run_all.

Description of the change

This starts up a RabbitMQ server when using make run or make
run_all. It also moves the RabbitServer fixture off into its own
package.

A point of controversy: the hostname and port for the RabbitMQ service
is hard-coded into schema-lazr.conf. Rob has said that he wants this
to be dynamically allocated, and indeed rabbitfixture knows how to do
that already if you ask it, but it makes it much harder for other
processes - outside of the appserver started by make run/run_all - to
know where to connect.

I don't know of an existing way to do this so that it just works -
although I'm sure everyone has ideas - and I think it's important not
to increase development friction while we wait for it. Choosing a
fixed port is no worse than the existing hard-coded ports we have for
things like HTTP, and it gets us up and running and *developing*.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

You write out a unique config fragment and export LPCONFIG before
starting services depending on rabbit. We do this in the test runner
and it works well - is there any reason it won't work here?

Revision history for this message
Gavin Panella (allenap) wrote :

I was thinking of something like:

$ make run_all &
$ make sync_branches

How do the scripts that sync_branches invokes know where Rabbit is?

Revision history for this message
Robert Collins (lifeless) wrote :

$ make run_all &
Assigned LPCONFIG=12345
$ export LPCONFIG=12345
$ make sync_branches

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

In case you want to address Robert's notes in a separate branch, I'm giving you my approval vote for this one—with comments:

41 === modified file 'lib/canonical/launchpad/scripts/runlaunchpad.py'
42 --- lib/canonical/launchpad/scripts/runlaunchpad.py 2011-03-07 16:32:12 +0000
43 +++ lib/canonical/launchpad/scripts/runlaunchpad.py 2011-09-13 15:01:18 +0000

64 @@ -221,6 +223,20 @@
65 process.stdin.close()
66
67
68 +class RabbitService(Service):
69 + """A RabbitMQ service."""
70 +
71 + @property
72 + def should_launch(self):
73 + return config.rabbitmq.launch
74 +
75 + def launch(self):
76 + hostname, port = config.rabbitmq.host.split(":")
77 + self.server = RabbitServer(
78 + RabbitServerResources(hostname=hostname, port=int(port)))
79 + self.useFixture(self.server)

Anything sensible to be done if no port is specified?

168 === added file 'lib/lp/services/rabbit/tests/test_server.py'
169 --- lib/lp/services/rabbit/tests/test_server.py 1970-01-01 00:00:00 +0000
170 +++ lib/lp/services/rabbit/tests/test_server.py 2011-09-13 15:01:18 +0000
171 @@ -0,0 +1,36 @@
172 +# Copyright 2011 Canonical Ltd. This software is licensed under the
173 +# GNU Affero General Public License version 3 (see the file LICENSE).
174 +
175 +"""Tests for lp.testing.fixture."""
176 +
177 +__metaclass__ = type
178 +
179 +from textwrap import dedent
180 +
181 +from fixtures import EnvironmentVariableFixture
182 +
183 +from canonical.testing.layers import BaseLayer
184 +from lp.services.rabbit.server import RabbitServer
185 +from lp.testing import TestCase
186 +
187 +
188 +class TestRabbitServer(TestCase):
189 +
190 + layer = BaseLayer
191 +
192 + def test_service_config(self):
193 + # Rabbit needs to fully isolate itself: an existing per user
194 + # .erlang.cookie has to be ignored, and ditto bogus HOME if other
195 + # tests fail to cleanup.
196 + self.useFixture(EnvironmentVariableFixture('HOME', '/nonsense/value'))
197 +
198 + # RabbitServer pokes some .ini configuration into its config.
199 + fixture = self.useFixture(RabbitServer())
200 + expected = dedent("""\
201 + [rabbitmq]
202 + host: localhost:%d
203 + userid: guest
204 + password: guest
205 + virtual_host: /
206 + """ % fixture.config.port)
207 + self.assertEqual(expected, fixture.config.service_config)

It seems unnecessarily brittle to test for the exact text. Otherwise you're really just repeating what the code does in the test. Plus, what of item ordering? What of whitespace? It's nice to have some room for cosmetic editing etc. without breaking tests.

To me it'd make more sense to parse the config snippet and assert the presence and value of the keys you care about.

Jeroen

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> $ make run_all &
> Assigned LPCONFIG=12345
> $ export LPCONFIG=12345
> $ make sync_branches

To me this would be unacceptable. Development is complicated enough.

Adding an extra step means another thing to remember, or another wiki
page to read, and the "Assigned LPCONFIG=12345" line would be mixed
into other log lines, or disappear off the scrollback.

Perhaps we could do something better:

- config.push() could also write out the fragments its given to
  configs/$LPCONFIG/extra-${sequence}-${name}.conf

- configs.pop() would remove the fragment from the filesystem too.

- canonical.config would automatically know to look for these extra-*
  files and push them in ${sequence} order.

- configs/*/extra-* could be added to .bzrignore

- make clean could remove them

Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, Sep 15, 2011 at 5:07 AM, Gavin Panella
<email address hidden> wrote:

> Perhaps we could do something better:
>
> - config.push() could also write out the fragments its given to
>  configs/$LPCONFIG/extra-${sequence}-${name}.conf

We might want a special verb for this to avoid it happening my mistake
on production servers. Make clean probably would want to delete them
all. The outer most test layer may want to zap them all as it comes up
as well.

Other than that this sounds good.

-Rob

Revision history for this message
Gavin Panella (allenap) wrote :

> In case you want to address Robert's notes in a separate branch, I'm giving
> you my approval vote for this one—with comments:

Thanks Jeroen :)

[...]
> Anything sensible to be done if no port is specified?

I've changed this to use lazr.config.as_host_port(...,
default_host=None, default_port=None). The effect of this is that the
code in rabbitfixture will revert to its defaults if either or both
are not specified: localhost and a dynamically allocated port.

[...]
> It seems unnecessarily brittle to test for the exact text. Otherwise you're
> really just repeating what the code does in the test. Plus, what of item
> ordering? What of whitespace? It's nice to have some room for cosmetic
> editing etc. without breaking tests.
>
> To me it'd make more sense to parse the config snippet and assert the presence
> and value of the keys you care about.

I've followed your advice; the test now parses the snippet and checks
all the sections and keys.

Revision history for this message
Gavin Panella (allenap) wrote :

> On Thu, Sep 15, 2011 at 5:07 AM, Gavin Panella <...> wrote:
>
> > Perhaps we could do something better:
> >
> > - config.push() could also write out the fragments its given to
> >  configs/$LPCONFIG/extra-${sequence}-${name}.conf
>
> We might want a special verb for this to avoid it happening my mistake
> on production servers.

By special verb, do you mean an alternative to .push() and .pop()? If
so, it might instead be worth having the special verb for pushing and
popping outside of dev/test; there are only a few places where these
methods are used outside of test/dev code:

  lib/lp/registry/scripts/distributionmirror_prober.py
  lib/canonical/launchpad/scripts/__init__.py
  lib/canonical/testing/layers.py
  lib/canonical/database/sqlbase.py

(In addition, .push() and .pop() could be disallowed - or warned about
- in non-dev/test environments.)

> Make clean probably would want to delete them all.

Agreed.

> The outer most test layer may want to zap them all as it comes up as
> well.

Agreed, thanks for thinking of that.

> Other than that this sounds good.

Tip top, thank you. Do you *need* me to do that before landing this
branch, or can I land this with a promise to sort out this new idea
asap? Red Squad are going to be working on longpoll as of now so even
a day or two's delay on this is going to affect that.

Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, Sep 15, 2011 at 7:39 AM, Gavin Panella
<email address hidden> wrote:

> Tip top, thank you. Do you *need* me to do that before landing this
> branch, or can I land this with a promise to sort out this new idea
> asap? Red Squad are going to be working on longpoll as of now so even
> a day or two's delay on this is going to affect that.

asap is fine.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2011-09-10 09:01:08 +0000
3+++ Makefile 2011-09-15 19:55:38 +0000
4@@ -253,7 +253,7 @@
5 $(PY) cronscripts/merge-proposal-jobs.py -v
6
7 run: build inplace stop
8- bin/run -r librarian,google-webservice,memcached -i $(LPCONFIG)
9+ bin/run -r librarian,google-webservice,memcached,rabbitmq -i $(LPCONFIG)
10
11 run.gdb:
12 echo 'run' > run.gdb
13@@ -265,7 +265,7 @@
14
15 run_all: build inplace stop
16 bin/run \
17- -r librarian,sftp,forker,mailman,codebrowse,google-webservice,memcached \
18+ -r librarian,sftp,forker,mailman,codebrowse,google-webservice,memcached,rabbitmq \
19 -i $(LPCONFIG)
20
21 run_codebrowse: build
22
23=== modified file 'configs/development/launchpad-lazr.conf'
24--- configs/development/launchpad-lazr.conf 2011-08-14 23:11:45 +0000
25+++ configs/development/launchpad-lazr.conf 2011-09-15 19:55:38 +0000
26@@ -239,6 +239,13 @@
27 host_key_private=lib/lp/poppy/tests/poppy-sftp
28 host_key_public=lib/lp/poppy/tests/poppy-sftp.pub
29
30+[rabbitmq]
31+launch: True
32+host: localhost:56720
33+userid: guest
34+password: guest
35+virtual_host: /
36+
37 [reclaimbranchspace]
38 error_dir: /var/tmp/codehosting.test
39 oops_prefix: RBS
40
41=== modified file 'configs/testrunner/launchpad-lazr.conf'
42--- configs/testrunner/launchpad-lazr.conf 2011-09-05 15:42:27 +0000
43+++ configs/testrunner/launchpad-lazr.conf 2011-09-15 19:55:38 +0000
44@@ -201,6 +201,13 @@
45 oops_prefix: TAPPORTBLOB
46 error_dir: /var/tmp/lperr.test
47
48+[rabbitmq]
49+launch: False
50+host: none
51+userid: none
52+password: none
53+virtual_host: none
54+
55 [request_daily_builds]
56 oops_prefix: TRDB
57 error_dir: /var/tmp/lperr.test
58
59=== modified file 'lib/canonical/config/schema-lazr.conf'
60--- lib/canonical/config/schema-lazr.conf 2011-09-02 16:22:48 +0000
61+++ lib/canonical/config/schema-lazr.conf 2011-09-15 19:55:38 +0000
62@@ -511,7 +511,8 @@
63 forced_hostname: none
64
65 # Where to find the code import scheduler service.
66-codeimportscheduler_url: http://xmlrpc-private.launchpad.dev:8087/codeimportscheduler
67+codeimportscheduler_url:
68+ http://xmlrpc-private.launchpad.dev:8087/codeimportscheduler
69
70 # The maximum number of jobs to run on a machine at one time.
71 max_jobs_per_machine: 3
72@@ -585,7 +586,8 @@
73 # The database user which will be used by this process.
74 # datatype: string
75 dbuser: rosettaadmin
76-source_interface: lp.translations.interfaces.translationpackagingjob.ITranslationPackagingJobSource
77+source_interface:
78+ lp.translations.interfaces.translationpackagingjob.ITranslationPackagingJobSource
79
80 # See [error_reports].
81 error_dir: none
82@@ -950,7 +952,8 @@
83
84 [initializedistroseries]
85 dbuser: initializedistroseries
86-source_interface: lp.soyuz.interfaces.distributionjob.IInitializeDistroSeriesJobSource
87+source_interface:
88+ lp.soyuz.interfaces.distributionjob.IInitializeDistroSeriesJobSource
89
90 # See [error_reports].
91 error_dir: none
92@@ -1025,7 +1028,8 @@
93 # domains listed here will be sent to the parent domain,
94 # allowing sessions to be shared between vhosts.
95 # Domains should not start with a leading '.'.
96-cookie_domains: demo.launchpad.net, staging.launchpad.net, launchpad.net, launchpad.dev
97+cookie_domains:
98+ demo.launchpad.net, staging.launchpad.net, launchpad.net, launchpad.dev
99
100 # Maximum size of product release download files in bytes. A value
101 # of 0 means no limit.
102@@ -1702,15 +1706,18 @@
103 memory_profile_log:
104
105 [rabbitmq]
106+# Should RabbitMQ be launched by default?
107+# datatype: boolean
108+launch: False
109 # The host:port at which RabbitMQ is listening.
110 # datatype: string
111 host: none
112 # datatype: string
113-userid: guest
114-# datatype: string
115-password: guest
116-# datatype: string
117-virtual_host: /
118+userid: none
119+# datatype: string
120+password: none
121+# datatype: string
122+virtual_host: none
123
124 [reclaimbranchspace]
125 # The database user which will be used by this process.
126
127=== modified file 'lib/canonical/launchpad/scripts/runlaunchpad.py'
128--- lib/canonical/launchpad/scripts/runlaunchpad.py 2011-03-07 16:32:12 +0000
129+++ lib/canonical/launchpad/scripts/runlaunchpad.py 2011-09-15 19:55:38 +0000
130@@ -14,6 +14,8 @@
131 import sys
132
133 import fixtures
134+from lazr.config import as_host_port
135+from rabbitfixture.server import RabbitServerResources
136 from zope.app.server.main import main
137
138 from canonical.config import config
139@@ -22,9 +24,10 @@
140 make_pidfile,
141 pidfile_path,
142 )
143+from lp.services.googlesearch import googletestservice
144 from lp.services.mailman import runmailman
145 from lp.services.osutils import ensure_directory_exists
146-from lp.services.googlesearch import googletestservice
147+from lp.services.rabbit.server import RabbitServer
148
149
150 def make_abspath(path):
151@@ -221,6 +224,20 @@
152 process.stdin.close()
153
154
155+class RabbitService(Service):
156+ """A RabbitMQ service."""
157+
158+ @property
159+ def should_launch(self):
160+ return config.rabbitmq.launch
161+
162+ def launch(self):
163+ hostname, port = as_host_port(config.rabbitmq.host, None, None)
164+ self.server = RabbitServer(
165+ RabbitServerResources(hostname=hostname, port=port))
166+ self.useFixture(self.server)
167+
168+
169 def stop_process(process):
170 """kill process and BLOCK until process dies.
171
172@@ -245,6 +262,7 @@
173 'codebrowse': CodebrowseService(),
174 'google-webservice': GoogleWebService(),
175 'memcached': MemcachedService(),
176+ 'rabbitmq': RabbitService(),
177 }
178
179
180@@ -287,8 +305,8 @@
181 """
182 if '-i' in args:
183 index = args.index('-i')
184- config.setInstance(args[index+1])
185- del args[index:index+2]
186+ config.setInstance(args[index + 1])
187+ del args[index:index + 2]
188
189 if '-C' not in args:
190 zope_config_file = config.zope_config_file
191@@ -319,10 +337,10 @@
192 if config.launchpad.launch:
193 main(argv)
194 else:
195- # We just need the foreground process to sit around forever waiting
196- # for the signal to shut everything down. Normally, Zope itself would
197- # be this master process, but we're not starting that up, so we need
198- # to do something else.
199+ # We just need the foreground process to sit around forever
200+ # waiting for the signal to shut everything down. Normally, Zope
201+ # itself would be this master process, but we're not starting that
202+ # up, so we need to do something else.
203 try:
204 signal.pause()
205 except KeyboardInterrupt:
206
207=== modified file 'lib/canonical/launchpad/scripts/tests/test_runlaunchpad.py'
208--- lib/canonical/launchpad/scripts/tests/test_runlaunchpad.py 2010-10-11 02:37:27 +0000
209+++ lib/canonical/launchpad/scripts/tests/test_runlaunchpad.py 2011-09-15 19:55:38 +0000
210@@ -152,6 +152,10 @@
211 if config.google_test_service.launch:
212 expected.append(SERVICES['google-webservice'])
213
214+ # RabbitMQ may or may not be asked to run.
215+ if config.rabbitmq.launch:
216+ expected.append(SERVICES['rabbitmq'])
217+
218 expected = sorted(expected)
219 self.assertEqual(expected, services)
220
221
222=== modified file 'lib/canonical/testing/layers.py'
223--- lib/canonical/testing/layers.py 2011-09-13 08:44:05 +0000
224+++ lib/canonical/testing/layers.py 2011-09-15 19:55:38 +0000
225@@ -143,13 +143,13 @@
226 import lp.services.mail.stub
227 from lp.services.memcache.client import memcache_client_factory
228 from lp.services.osutils import kill_by_pidfile
229+from lp.services.rabbit.server import RabbitServer
230 from lp.testing import (
231 ANONYMOUS,
232 is_logged_in,
233 login,
234 logout,
235 )
236-from lp.testing.fixture import RabbitServer
237 from lp.testing.pgsql import PgTestSetup
238
239
240
241=== added directory 'lib/lp/services/rabbit'
242=== added file 'lib/lp/services/rabbit/__init__.py'
243=== added file 'lib/lp/services/rabbit/server.py'
244--- lib/lp/services/rabbit/server.py 1970-01-01 00:00:00 +0000
245+++ lib/lp/services/rabbit/server.py 2011-09-15 19:55:38 +0000
246@@ -0,0 +1,31 @@
247+# Copyright 2011 Canonical Ltd. This software is licensed under the
248+# GNU Affero General Public License version 3 (see the file LICENSE).
249+
250+"""RabbitMQ server fixture."""
251+
252+__metaclass__ = type
253+__all__ = [
254+ 'RabbitServer',
255+ ]
256+
257+from textwrap import dedent
258+
259+import rabbitfixture.server
260+
261+
262+class RabbitServer(rabbitfixture.server.RabbitServer):
263+ """A RabbitMQ server fixture with Launchpad-specific config.
264+
265+ :ivar service_config: A snippet of .ini that describes the `rabbitmq`
266+ configuration.
267+ """
268+
269+ def setUp(self):
270+ super(RabbitServer, self).setUp()
271+ self.config.service_config = dedent("""\
272+ [rabbitmq]
273+ host: localhost:%d
274+ userid: guest
275+ password: guest
276+ virtual_host: /
277+ """ % self.config.port)
278
279=== added directory 'lib/lp/services/rabbit/tests'
280=== added file 'lib/lp/services/rabbit/tests/__init__.py'
281=== added file 'lib/lp/services/rabbit/tests/test_server.py'
282--- lib/lp/services/rabbit/tests/test_server.py 1970-01-01 00:00:00 +0000
283+++ lib/lp/services/rabbit/tests/test_server.py 2011-09-15 19:55:38 +0000
284@@ -0,0 +1,40 @@
285+# Copyright 2011 Canonical Ltd. This software is licensed under the
286+# GNU Affero General Public License version 3 (see the file LICENSE).
287+
288+"""Tests for lp.testing.fixture."""
289+
290+__metaclass__ = type
291+
292+from ConfigParser import SafeConfigParser
293+from StringIO import StringIO
294+
295+from fixtures import EnvironmentVariableFixture
296+
297+from canonical.testing.layers import BaseLayer
298+from lp.services.rabbit.server import RabbitServer
299+from lp.testing import TestCase
300+
301+
302+class TestRabbitServer(TestCase):
303+
304+ layer = BaseLayer
305+
306+ def test_service_config(self):
307+ # Rabbit needs to fully isolate itself: an existing per user
308+ # .erlang.cookie has to be ignored, and ditto bogus HOME if other
309+ # tests fail to cleanup.
310+ self.useFixture(EnvironmentVariableFixture('HOME', '/nonsense/value'))
311+
312+ # RabbitServer pokes some .ini configuration into its config.
313+ fixture = self.useFixture(RabbitServer())
314+ service_config = SafeConfigParser()
315+ service_config.readfp(StringIO(fixture.config.service_config))
316+ self.assertEqual(["rabbitmq"], service_config.sections())
317+ expected = {
318+ "host": "localhost:%d" % fixture.config.port,
319+ "userid": "guest",
320+ "password": "guest",
321+ "virtual_host": "/",
322+ }
323+ observed = dict(service_config.items("rabbitmq"))
324+ self.assertEqual(expected, observed)
325
326=== modified file 'lib/lp/testing/fixture.py'
327--- lib/lp/testing/fixture.py 2011-09-05 16:39:46 +0000
328+++ lib/lp/testing/fixture.py 2011-09-15 19:55:38 +0000
329@@ -5,7 +5,6 @@
330
331 __metaclass__ = type
332 __all__ = [
333- 'RabbitServer',
334 'ZopeAdapterFixture',
335 'ZopeEventHandlerFixture',
336 'ZopeViewReplacementFixture',
337@@ -13,14 +12,12 @@
338
339 from ConfigParser import SafeConfigParser
340 import os.path
341-from textwrap import dedent
342
343 from fixtures import (
344 EnvironmentVariableFixture,
345 Fixture,
346 )
347 import pgbouncer.fixture
348-import rabbitfixture.server
349 from zope.component import (
350 getGlobalSiteManager,
351 provideHandler,
352@@ -36,24 +33,6 @@
353 from canonical.config import config
354
355
356-class RabbitServer(rabbitfixture.server.RabbitServer):
357- """A RabbitMQ server fixture with Launchpad-specific config.
358-
359- :ivar service_config: A snippet of .ini that describes the `rabbitmq`
360- configuration.
361- """
362-
363- def setUp(self):
364- super(RabbitServer, self).setUp()
365- self.config.service_config = dedent("""\
366- [rabbitmq]
367- host: localhost:%d
368- userid: guest
369- password: guest
370- virtual_host: /
371- """ % self.config.port)
372-
373-
374 class PGBouncerFixture(pgbouncer.fixture.PGBouncerFixture):
375 """Inserts a controllable pgbouncer instance in front of PostgreSQL.
376
377
378=== modified file 'lib/lp/testing/tests/test_fixture.py'
379--- lib/lp/testing/tests/test_fixture.py 2011-09-05 15:42:27 +0000
380+++ lib/lp/testing/tests/test_fixture.py 2011-09-15 19:55:38 +0000
381@@ -5,9 +5,6 @@
382
383 __metaclass__ = type
384
385-from textwrap import dedent
386-
387-from fixtures import EnvironmentVariableFixture
388 import psycopg2
389 from storm.exceptions import DisconnectionError
390 from zope.component import (
391@@ -30,33 +27,10 @@
392 from lp.testing import TestCase
393 from lp.testing.fixture import (
394 PGBouncerFixture,
395- RabbitServer,
396 ZopeAdapterFixture,
397 )
398
399
400-class TestRabbitServer(TestCase):
401-
402- layer = BaseLayer
403-
404- def test_service_config(self):
405- # Rabbit needs to fully isolate itself: an existing per user
406- # .erlange.cookie has to be ignored, and ditto bogus HOME if other
407- # tests fail to cleanup.
408- self.useFixture(EnvironmentVariableFixture('HOME', '/nonsense/value'))
409-
410- # RabbitServer pokes some .ini configuration into its config.
411- with RabbitServer() as fixture:
412- expected = dedent("""\
413- [rabbitmq]
414- host: localhost:%d
415- userid: guest
416- password: guest
417- virtual_host: /
418- """ % fixture.config.port)
419- self.assertEqual(expected, fixture.config.service_config)
420-
421-
422 class IFoo(Interface):
423 pass
424