Merge lp:~flacoste/launchpad/bug-801233 into lp:launchpad

Proposed by Francis J. Lacoste
Status: Merged
Approved by: Francis J. Lacoste
Approved revision: no longer in the source branch.
Merged at revision: 13501
Proposed branch: lp:~flacoste/launchpad/bug-801233
Merge into: lp:launchpad
Diff against target: 1343 lines (+617/-83)
25 files modified
lib/canonical/launchpad/configure.zcml (+1/-0)
lib/lp/app/browser/launchpad.py (+5/-1)
lib/lp/buildmaster/configure.zcml (+1/-0)
lib/lp/buildmaster/interfaces/builder.py (+36/-19)
lib/lp/buildmaster/model/builder.py (+13/-10)
lib/lp/buildmaster/security.py (+19/-0)
lib/lp/buildmaster/tests/test_webservice.py (+68/-0)
lib/lp/services/webservice/configure.zcml (+15/-0)
lib/lp/services/webservice/json.py (+24/-0)
lib/lp/services/webservice/tests/test_json.py (+30/-0)
lib/lp/services/webservice/wadl-to-refhtml.xsl (+126/-6)
lib/lp/soyuz/browser/configure.zcml (+7/-5)
lib/lp/soyuz/browser/processor.py (+8/-14)
lib/lp/soyuz/browser/tests/test_processor.py (+41/-0)
lib/lp/soyuz/configure.zcml (+7/-0)
lib/lp/soyuz/interfaces/processor.py (+34/-4)
lib/lp/soyuz/interfaces/webservice.py (+2/-6)
lib/lp/soyuz/model/processor.py (+21/-0)
lib/lp/soyuz/security.py (+26/-0)
lib/lp/soyuz/tests/test_packagecloner.py (+1/-4)
lib/lp/soyuz/tests/test_processor.py (+90/-7)
lib/lp/testing/__init__.py (+11/-0)
lib/lp/testing/factory.py (+6/-3)
lib/lp/testing/publication.py (+11/-3)
lib/lp/testing/tests/test_publication.py (+14/-1)
To merge this branch: bzr merge lp:~flacoste/launchpad/bug-801233
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+67353@code.launchpad.net

Commit message

[r=bac][bug=801233] Export IBuilderSet.getBuildQueueSize(), getBuildersForQueue() and IBuilder.architecture over the API. Change the URL to IProcessor to be more user-friendly. Bonus: add proper URL entry to API documentation for many entry types. More bonus: Include :return: and :raise: docutils documentation for custom methods in the webservice documentation.

Description of the change

= Summary =

This is a massive Yak shaving branch! The goal is to write an API script which
will allow copy rebuilds to be manually allocated a number of builders. To do
this, we need to find dynamically the number of available builders for a
particular processor. That information isn't currently available over the
API.

== Proposed fix ==

It exports IBuilder.processor, IBuilderSet.getBuidQueueSizes() and
IBuilderSet.getBuilderrsForQueue(). Along with numerous other yak that needed
shaving along the way. An important one worth mentioning here: I changed the
URL of IProcessor. They were living under their family and their DB id was
part of th URL. Since IProcessor name are globally unique, I added a
/+processors collection and their URL point their using their name. That will
make it easier to get processor by crafting the URL.

== Pre-implementation notes ==

No pre-implementation done.

== Implementation details ==

Here are the numerous yak shaved along the way:

 * Removed getBuildersByArch() which wasn't used anywhere.
 * getBuildQueueSize() returns a dict containing datetime.timedelta object.
 simplejson didn't know how to serialize those, so I hooked up a very
 simple one which simply str() it. That was added into
 lp.services.webservice.json and hooked through zcml.
 * Collections check that launchpad.View is available on the entry. So I had
 to define adapters for IBuilder, IProcessor and IProcessorFamily.
 * Since there is no web page for IProcessor nor IProcessorFamily, I changed
 pulish_web_link to False.

Everything else should be straightforward, let me know if you have any
questions.

== Tests ==

 ./bin/test -vvt
'lp.soyuz.tests.test_processor|lp.soyuz.browser.tests.test_processor|lp.buildmaster.tests.test_webservice'

== Demo and Q/A ==

This will be QA-ed by checking the exported attribute and methods on
qastaging.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/buildmaster/model/builder.py
  lib/lp/soyuz/tests/test_processor.py
  lib/lp/services/webservice/tests/__init__.py
  lib/lp/buildmaster/security.py
  lib/lp/soyuz/model/processor.py
  lib/lp/soyuz/interfaces/processor.py
  lib/lp/soyuz/security.py
  lib/lp/app/browser/launchpad.py
  lib/lp/testing/publication.py
  lib/lp/soyuz/configure.zcml
  lib/lp/testing/factory.py
  lib/lp/testing/__init__.py
  lib/lp/soyuz/interfaces/webservice.py
  lib/lp/buildmaster/configure.zcml
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/services/webservice/tests/test_json.py
  lib/lp/testing/tests/test_publication.py
  lib/canonical/launchpad/configure.zcml
  lib/lp/services/webservice/json.py
  lib/lp/soyuz/browser/processor.py
  lib/lp/services/webservice/configure.zcml
  lib/lp/buildmaster/tests/test_webservice.py
  lib/lp/buildmaster/interfaces/builder.py
  lib/lp/soyuz/browser/tests/test_processor.py

./lib/lp/buildmaster/model/builder.py
     174: Line exceeds 78 characters.
     261: Line exceeds 78 characters.
     219: W291 trailing whitespace
     284: E301 expected 1 blank line, found 0
     302: E301 expected 1 blank line, found 0
     373: E301 expected 1 blank line, found 0
     517: E301 expected 1 blank line, found 0
     519: E301 expected 1 blank line, found 0
     608: E301 expected 1 blank line, found 0
     683: E301 expected 1 blank line, found 0
     686: E301 expected 1 blank line, found 0
./lib/lp/soyuz/tests/test_processor.py
      48: Line exceeds 78 characters.
      55: Line exceeds 78 characters.
      57: Line exceeds 78 characters.
     123: Line exceeds 78 characters.
      88: E202 whitespace before ')'
     123: E501 line too long (123 characters)
     123: E202 whitespace before ')'
./lib/lp/soyuz/model/processor.py
      87: E301 expected 1 blank line, found 0
./lib/lp/soyuz/security.py
      18: E302 expected 2 blank lines, found 1
      22: E302 expected 2 blank lines, found 1
./lib/lp/testing/publication.py
      36: E302 expected 2 blank lines, found 1
./lib/lp/services/webservice/tests/test_json.py
      27: E303 too many blank lines (2)
      30: W291 trailing whitespace
      33: W391 blank line at end of file
./lib/lp/testing/tests/test_publication.py
      49: E301 expected 1 blank line, found 0
      58: W291 trailing whitespace
      79: E301 expected 1 blank line, found 0
     101: E301 expected 1 blank line, found 0
./lib/lp/buildmaster/interfaces/builder.py
     139: Line exceeds 78 characters.
     210: Line exceeds 78 characters.
     249: W293 blank line contains whitespace
     256: W293 blank line contains whitespace
./lib/lp/soyuz/browser/tests/test_processor.py
      13: E302 expected 2 blank lines, found 1
      41: W391 blank line at end of file

I'll fix the white-space lint post-review.

To post a comment you must log in.
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Another peculiar I forgot about: the test_traverse() helper wasn't working for API requests, I fixed. that.

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Francis,

This is a really good follow up branch to the changes I made. Thanks for making the processor traversal by name.

I only found a few things to note:

* As we discussed on IRC the docstring for getBuildQueueSizes make the wadl generator unhappy.

* In lib/lp/services/webservice/json.py you should add StrJSONSerializer to __all__.

* In lib/lp/testing/factory.py, s/same name than the family./same name as the family./

* Some of the lint is real, including new code with super long lines such as in test_default_collection.

* in ProcessorSet getAll and getByName why don't you use the storeOf(self)?

review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On 11-07-11 03:43 PM, Brad Crittenden wrote:
> Review: Approve code
> Hi Francis,
>
> This is a really good follow up branch to the changes I made. Thanks for making the processor traversal by name.
>
> I only found a few things to note:
>
> * As we discussed on IRC the docstring for getBuildQueueSizes make the wadl generator unhappy.

Yeah, this was a simple matter of using the rst literal block directive
to make it happy. (::)

This introduces a few other drive-bys:

* Turned out that our WADL to HTML conversion excludes the RST
parameters table (because we don't want the :param name: documentation
to be redundant or misleading with the web-service specific
documentation.) So I added support for the :return: (and :raise:)
parameters for custom methods (when appropriate). This will make the
return value of a couple other JSON-returning method documentated.

* Got bored and tired of seeing so many Unkonwn URL entry messages, so I
fixed a bunch. Not all of them are fixed. Will eventually chase the
others, but it's still an improvement!

>
> * In lib/lp/services/webservice/json.py you should add StrJSONSerializer to __all__.

Done.

>
> * In lib/lp/testing/factory.py, s/same name than the family./same name as the family./
>

Done.

> * Some of the lint is real, including new code with super long lines such as in test_default_collection.

Branch is now lint-free!

>
> * in ProcessorSet getAll and getByName why don't you use the storeOf(self)?

Because self isn't a DB instance (and thus doesn't have a store).
(That's usually the case in *Set object.)

Thanks for the review!
--
Francis J. Lacoste
<email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml 2011-02-23 21:15:23 +0000
+++ lib/canonical/launchpad/configure.zcml 2011-07-21 19:34:45 +0000
@@ -27,6 +27,7 @@
27 <include package="lp.coop.answersbugs" />27 <include package="lp.coop.answersbugs" />
28 <include package="lp.code" />28 <include package="lp.code" />
29 <include package="lp.soyuz" />29 <include package="lp.soyuz" />
30 <include package="lp.services.webservice" />
30 <include package="lp.translations" />31 <include package="lp.translations" />
31 <include package="lp.testopenid" />32 <include package="lp.testopenid" />
32 <include package="lp.blueprints" />33 <include package="lp.blueprints" />
3334
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2011-06-30 11:28:59 +0000
+++ lib/lp/app/browser/launchpad.py 2011-07-21 19:34:45 +0000
@@ -146,7 +146,10 @@
146from lp.services.worlddata.interfaces.language import ILanguageSet146from lp.services.worlddata.interfaces.language import ILanguageSet
147from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet147from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
148from lp.soyuz.interfaces.packageset import IPackagesetSet148from lp.soyuz.interfaces.packageset import IPackagesetSet
149from lp.soyuz.interfaces.processor import IProcessorFamilySet149from lp.soyuz.interfaces.processor import (
150 IProcessorFamilySet,
151 IProcessorSet,
152 )
150from lp.testopenid.interfaces.server import ITestOpenIDApplication153from lp.testopenid.interfaces.server import ITestOpenIDApplication
151from lp.translations.interfaces.translationgroup import ITranslationGroupSet154from lp.translations.interfaces.translationgroup import ITranslationGroupSet
152from lp.translations.interfaces.translationimportqueue import (155from lp.translations.interfaces.translationimportqueue import (
@@ -617,6 +620,7 @@
617 'people': IPersonSet,620 'people': IPersonSet,
618 'pillars': IPillarNameSet,621 'pillars': IPillarNameSet,
619 '+processor-families': IProcessorFamilySet,622 '+processor-families': IProcessorFamilySet,
623 '+processors': IProcessorSet,
620 'projects': IProductSet,624 'projects': IProductSet,
621 'projectgroups': IProjectGroupSet,625 'projectgroups': IProjectGroupSet,
622 'sourcepackagenames': ISourcePackageNameSet,626 'sourcepackagenames': ISourcePackageNameSet,
623627
=== modified file 'lib/lp/buildmaster/configure.zcml'
--- lib/lp/buildmaster/configure.zcml 2010-11-19 13:25:25 +0000
+++ lib/lp/buildmaster/configure.zcml 2011-07-21 19:34:45 +0000
@@ -10,6 +10,7 @@
10 xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"10 xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
11 i18n_domain="launchpad">11 i18n_domain="launchpad">
12 <include package=".browser"/>12 <include package=".browser"/>
13 <authorizations module=".security" />
1314
14 <!-- Builder -->15 <!-- Builder -->
15 <class16 <class
1617
=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py 2011-02-24 15:30:54 +0000
+++ lib/lp/buildmaster/interfaces/builder.py 2011-07-21 19:34:45 +0000
@@ -27,6 +27,12 @@
27 exported,27 exported,
28 operation_parameters,28 operation_parameters,
29 operation_returns_entry,29 operation_returns_entry,
30 operation_returns_collection_of,
31 operation_for_version,
32 )
33from lazr.restful.fields import (
34 Reference,
35 ReferenceChoice,
30 )36 )
31from zope.interface import (37from zope.interface import (
32 Attribute,38 Attribute,
@@ -34,7 +40,6 @@
34 )40 )
35from zope.schema import (41from zope.schema import (
36 Bool,42 Bool,
37 Choice,
38 Field,43 Field,
39 Int,44 Int,
40 Text,45 Text,
@@ -45,6 +50,7 @@
45from lp.app.validators.name import name_validator50from lp.app.validators.name import name_validator
46from lp.app.validators.url import builder_url_validator51from lp.app.validators.url import builder_url_validator
47from lp.registry.interfaces.role import IHasOwner52from lp.registry.interfaces.role import IHasOwner
53from lp.soyuz.interfaces.processor import IProcessor
48from lp.services.fields import (54from lp.services.fields import (
49 Description,55 Description,
50 PersonChoice,56 PersonChoice,
@@ -106,10 +112,12 @@
106112
107 id = Attribute("Builder identifier")113 id = Attribute("Builder identifier")
108114
109 processor = Choice(115 processor = exported(ReferenceChoice(
110 title=_('Processor'), required=True, vocabulary='Processor',116 title=_('Processor'), required=True, vocabulary='Processor',
117 schema=IProcessor,
111 description=_('Build Slave Processor, used to identify '118 description=_('Build Slave Processor, used to identify '
112 'which jobs can be built by this device.'))119 'which jobs can be built by this device.')),
120 as_of='devel', readonly=True)
113121
114 owner = exported(PersonChoice(122 owner = exported(PersonChoice(
115 title=_('Owner'), required=True, vocabulary='ValidOwner',123 title=_('Owner'), required=True, vocabulary='ValidOwner',
@@ -128,7 +136,8 @@
128136
129 title = exported(Title(137 title = exported(Title(
130 title=_('Title'), required=True,138 title=_('Title'), required=True,
131 description=_('The builder slave title. Should be just a few words.')))139 description=_(
140 'The builder slave title. Should be just a few words.')))
132141
133 description = exported(Description(142 description = exported(Description(
134 title=_('Description'), required=False,143 title=_('Description'), required=False,
@@ -199,8 +208,8 @@
199 :param file_sha1: The file's sha1, which is how the file is addressed208 :param file_sha1: The file's sha1, which is how the file is addressed
200 in the slave XMLRPC protocol. Specially, the file_sha1 'buildlog'209 in the slave XMLRPC protocol. Specially, the file_sha1 'buildlog'
201 will cause the build log to be retrieved and gzipped.210 will cause the build log to be retrieved and gzipped.
202 :param filename: The name of the file to be given to the librarian file211 :param filename: The name of the file to be given to the librarian
203 alias.212 file alias.
204 :param private: True if the build is for a private archive.213 :param private: True if the build is for a private archive.
205 :return: A Deferred that calls back with a librarian file alias.214 :return: A Deferred that calls back with a librarian file alias.
206 """215 """
@@ -238,14 +247,14 @@
238247
239 def updateStatus(logger=None):248 def updateStatus(logger=None):
240 """Update the builder's status by probing it.249 """Update the builder's status by probing it.
241 250
242 :return: A Deferred that fires when the dialog with the slave is251 :return: A Deferred that fires when the dialog with the slave is
243 finished. It does not have a return value.252 finished. It does not have a return value.
244 """253 """
245254
246 def cleanSlave():255 def cleanSlave():
247 """Clean any temporary files from the slave.256 """Clean any temporary files from the slave.
248 257
249 :return: A Deferred that fires when the dialog with the slave is258 :return: A Deferred that fires when the dialog with the slave is
250 finished. It does not have a return value.259 finished. It does not have a return value.
251 """260 """
@@ -385,26 +394,34 @@
385 def getBuilders():394 def getBuilders():
386 """Return all active configured builders."""395 """Return all active configured builders."""
387396
388 def getBuildersByArch(arch):397 @export_read_operation()
389 """Return all configured builders for a given DistroArchSeries."""398 @operation_for_version('devel')
390
391 def getBuildQueueSizes():399 def getBuildQueueSizes():
392 """Return the number of pending builds for each processor.400 """Return the number of pending builds for each processor.
393401
394 :return: a dict of tuples with the queue size and duration for402 :return: a dict of tuples with the queue size and duration for
395 each processor and virtualisation. For example:403 each processor and virtualisation. For example::
396 {404
397 'virt': {405 {
398 '386': (1, datetime.timedelta(0, 60)),406 'virt': {
399 'amd64': (2, datetime.timedelta(0, 30)),407 '386': (1, datetime.timedelta(0, 60)),
400 },408 'amd64': (2, datetime.timedelta(0, 30)),
401 'nonvirt':...409 },
402 }410 'nonvirt':...
411 }
403412
404 The tuple contains the size of the queue, as an integer,413 The tuple contains the size of the queue, as an integer,
405 and the sum of the jobs 'estimated_duration' in queue,414 and the sum of the jobs 'estimated_duration' in queue,
406 as a timedelta or None for empty queues.415 as a timedelta or None for empty queues.
407 """416 """
408417
418 @operation_parameters(
419 processor=Reference(
420 title=_("Processor"), required=True, schema=IProcessor),
421 virtualized=Bool(
422 title=_("Virtualized"), required=False, default=True))
423 @operation_returns_collection_of(IBuilder)
424 @export_read_operation()
425 @operation_for_version('devel')
409 def getBuildersForQueue(processor, virtualized):426 def getBuildersForQueue(processor, virtualized):
410 """Return all builders for given processor/virtualization setting."""427 """Return all builders for given processor/virtualization setting."""
411428
=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py 2011-02-01 18:14:57 +0000
+++ lib/lp/buildmaster/model/builder.py 2011-07-21 19:34:45 +0000
@@ -171,7 +171,8 @@
171171
172 :param builder_url: The URL of the slave buildd machine,172 :param builder_url: The URL of the slave buildd machine,
173 e.g. http://localhost:8221173 e.g. http://localhost:8221
174 :param vm_host: If the slave is virtual, specify its host machine here.174 :param vm_host: If the slave is virtual, specify its host machine
175 here.
175 :param reactor: Used by tests to override the Twisted reactor.176 :param reactor: Used by tests to override the Twisted reactor.
176 :param proxy: Used By tests to override the xmlrpc.Proxy.177 :param proxy: Used By tests to override the xmlrpc.Proxy.
177 """178 """
@@ -216,7 +217,7 @@
216 def getFile(self, sha_sum, file_to_write):217 def getFile(self, sha_sum, file_to_write):
217 """Fetch a file from the builder.218 """Fetch a file from the builder.
218219
219 :param sha_sum: The sha of the file (which is also its name on the 220 :param sha_sum: The sha of the file (which is also its name on the
220 builder)221 builder)
221 :param file_to_write: A file name or file-like object to write222 :param file_to_write: A file name or file-like object to write
222 the file to223 the file to
@@ -258,7 +259,8 @@
258 resume_command = config.builddmaster.vm_resume_command % {259 resume_command = config.builddmaster.vm_resume_command % {
259 'vm_host': self._vm_host}260 'vm_host': self._vm_host}
260 # Twisted API requires string but the configuration provides unicode.261 # Twisted API requires string but the configuration provides unicode.
261 resume_argv = [term.encode('utf-8') for term in resume_command.split()]262 resume_argv = [
263 term.encode('utf-8') for term in resume_command.split()]
262 d = defer.Deferred()264 d = defer.Deferred()
263 p = ProcessWithTimeout(265 p = ProcessWithTimeout(
264 d, config.builddmaster.socket_timeout, clock=clock)266 d, config.builddmaster.socket_timeout, clock=clock)
@@ -281,6 +283,7 @@
281 def sendFileToSlave(self, sha1, url, username="", password=""):283 def sendFileToSlave(self, sha1, url, username="", password=""):
282 """Helper to send the file at 'url' with 'sha1' to this builder."""284 """Helper to send the file at 'url' with 'sha1' to this builder."""
283 d = self.ensurepresent(sha1, url, username, password)285 d = self.ensurepresent(sha1, url, username, password)
286
284 def check_present((present, info)):287 def check_present((present, info)):
285 if not present:288 if not present:
286 raise CannotFetchFile(url, info)289 raise CannotFetchFile(url, info)
@@ -299,6 +302,7 @@
299 """302 """
300 d = self._with_timeout(self._server.callRemote(303 d = self._with_timeout(self._server.callRemote(
301 'build', buildid, builder_type, chroot_sha1, filemap, args))304 'build', buildid, builder_type, chroot_sha1, filemap, args))
305
302 def got_fault(failure):306 def got_fault(failure):
303 failure.trap(xmlrpclib.Fault)307 failure.trap(xmlrpclib.Fault)
304 raise BuildSlaveFailure(failure.value)308 raise BuildSlaveFailure(failure.value)
@@ -370,6 +374,7 @@
370 d = builder.cleanSlave()374 d = builder.cleanSlave()
371 else:375 else:
372 d = builder.requestAbort()376 d = builder.requestAbort()
377
373 def log_rescue(ignored):378 def log_rescue(ignored):
374 if logger:379 if logger:
375 logger.info(380 logger.info(
@@ -514,8 +519,10 @@
514 logger.info("Resuming %s (%s)" % (self.name, self.url))519 logger.info("Resuming %s (%s)" % (self.name, self.url))
515520
516 d = self.slave.resume()521 d = self.slave.resume()
522
517 def got_resume_ok((stdout, stderr, returncode)):523 def got_resume_ok((stdout, stderr, returncode)):
518 return stdout, stderr524 return stdout, stderr
525
519 def got_resume_bad(failure):526 def got_resume_bad(failure):
520 stdout, stderr, code = failure.value527 stdout, stderr, code = failure.value
521 raise CannotResumeHost(528 raise CannotResumeHost(
@@ -605,6 +612,7 @@
605 def slaveStatus(self):612 def slaveStatus(self):
606 """See IBuilder."""613 """See IBuilder."""
607 d = self.slave.status()614 d = self.slave.status()
615
608 def got_status(status_sentence):616 def got_status(status_sentence):
609 status = {'builder_status': status_sentence[0]}617 status = {'builder_status': status_sentence[0]}
610618
@@ -680,9 +688,11 @@
680 if not self.builderok:688 if not self.builderok:
681 return defer.succeed(False)689 return defer.succeed(False)
682 d = self.slaveStatusSentence()690 d = self.slaveStatusSentence()
691
683 def catch_fault(failure):692 def catch_fault(failure):
684 failure.trap(xmlrpclib.Fault, socket.error)693 failure.trap(xmlrpclib.Fault, socket.error)
685 return False694 return False
695
686 def check_available(status):696 def check_available(status):
687 return status[0] == BuilderStatus.IDLE697 return status[0] == BuilderStatus.IDLE
688 return d.addCallbacks(check_available, catch_fault)698 return d.addCallbacks(check_available, catch_fault)
@@ -896,13 +906,6 @@
896 return Builder.selectBy(906 return Builder.selectBy(
897 active=True, orderBy=['virtualized', 'processor', 'name'])907 active=True, orderBy=['virtualized', 'processor', 'name'])
898908
899 def getBuildersByArch(self, arch):
900 """See IBuilderSet."""
901 return Builder.select('builder.processor = processor.id '
902 'AND processor.family = %d'
903 % arch.processorfamily.id,
904 clauseTables=("Processor",))
905
906 def getBuildQueueSizes(self):909 def getBuildQueueSizes(self):
907 """See `IBuilderSet`."""910 """See `IBuilderSet`."""
908 store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)911 store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
909912
=== added file 'lib/lp/buildmaster/security.py'
--- lib/lp/buildmaster/security.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/security.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,19 @@
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"""Security adapters for the buildmaster package."""
5
6__metaclass__ = type
7__all__ = [
8 'ViewBuilder',
9 ]
10
11from lp.app.security import AnonymousAuthorization
12from lp.buildmaster.interfaces.builder import (
13 IBuilder,
14 )
15
16
17class ViewBuilder(AnonymousAuthorization):
18 """Anyone can view a `IBuilder`."""
19 usedfor = IBuilder
020
=== added file 'lib/lp/buildmaster/tests/test_webservice.py'
--- lib/lp/buildmaster/tests/test_webservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/tests/test_webservice.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,68 @@
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 the builders webservice ."""
5
6__metaclass__ = type
7
8from canonical.testing.layers import DatabaseFunctionalLayer
9from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
10from lp.testing import (
11 api_url,
12 logout,
13 TestCaseWithFactory,
14 )
15
16
17class TestBuildersCollection(TestCaseWithFactory):
18 layer = DatabaseFunctionalLayer
19
20 def setUp(self):
21 super(TestBuildersCollection, self).setUp()
22 self.webservice = LaunchpadWebServiceCaller()
23
24 def test_getBuildQueueSizes(self):
25 logout()
26 results = self.webservice.named_get(
27 '/builders', 'getBuildQueueSizes', api_version='devel')
28 self.assertEquals(
29 ['nonvirt', 'virt'], sorted(results.jsonBody().keys()))
30
31 def test_getBuildersForQueue(self):
32 g1 = self.factory.makeProcessorFamily('g1').processors[0]
33 quantum = self.factory.makeProcessorFamily('quantum').processors[0]
34 self.factory.makeBuilder(
35 processor=quantum, name='quantum_builder1')
36 self.factory.makeBuilder(
37 processor=quantum, name='quantum_builder2')
38 self.factory.makeBuilder(
39 processor=quantum, name='quantum_builder3', virtualized=False)
40 self.factory.makeBuilder(
41 processor=g1, name='g1_builder', virtualized=False)
42
43 logout()
44 results = self.webservice.named_get(
45 '/builders', 'getBuildersForQueue',
46 processor=api_url(quantum), virtualized=True,
47 api_version='devel').jsonBody()
48 self.assertEquals(
49 ['quantum_builder1', 'quantum_builder2'],
50 sorted(builder['name'] for builder in results['entries']))
51
52
53class TestBuilderEntry(TestCaseWithFactory):
54 layer = DatabaseFunctionalLayer
55
56 def setUp(self):
57 super(TestBuilderEntry, self).setUp()
58 self.webservice = LaunchpadWebServiceCaller()
59
60 def test_exports_processor(self):
61 processor_family = self.factory.makeProcessorFamily('s1')
62 builder = self.factory.makeBuilder(
63 processor=processor_family.processors[0])
64
65 logout()
66 entry = self.webservice.get(
67 api_url(builder), api_version='devel').jsonBody()
68 self.assertEndsWith(entry['processor_link'], '/+processors/s1')
069
=== added file 'lib/lp/services/webservice/configure.zcml'
--- lib/lp/services/webservice/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/configure.zcml 2011-07-21 19:34:45 +0000
@@ -0,0 +1,15 @@
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
5<configure
6 xmlns="http://namespaces.zope.org/zope"
7 xmlns:i18n="http://namespaces.zope.org/i18n"
8 i18n_domain="launchpad">
9
10 <adapter
11 provides="lazr.restful.interfaces.IJSONPublishable"
12 for="zope.interface.common.idatetime.ITimeDelta"
13 factory="lp.services.webservice.json.StrJSONSerializer"
14 permission="zope.Public"/>
15</configure>
016
=== added file 'lib/lp/services/webservice/json.py'
--- lib/lp/services/webservice/json.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/json.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,24 @@
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"""Additional JSON serializer for the web service."""
5
6__metaclass__ = type
7__all__ = [
8 'StrJSONSerializer',
9 ]
10
11
12from lazr.restful.interfaces import IJSONPublishable
13from zope.interface import implements
14
15
16class StrJSONSerializer:
17 """Simple JSON serializer that simply str() it's context. """
18 implements(IJSONPublishable)
19
20 def __init__(self, context):
21 self.context = context
22
23 def toDataForJSON(self, media_type):
24 return str(self.context)
025
=== added directory 'lib/lp/services/webservice/tests'
=== added file 'lib/lp/services/webservice/tests/__init__.py'
=== added file 'lib/lp/services/webservice/tests/test_json.py'
--- lib/lp/services/webservice/tests/test_json.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/tests/test_json.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,30 @@
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 the JSON serializer."""
5
6__metaclass__ = type
7
8from datetime import timedelta
9
10from canonical.testing.layers import FunctionalLayer
11from lazr.restful.interfaces import IJSONPublishable
12from lp.services.webservice.json import StrJSONSerializer
13from lp.testing import TestCase
14
15
16class TestStrJSONSerializer(TestCase):
17 layer = FunctionalLayer
18
19 def test_toDataForJSON(self):
20 serializer = StrJSONSerializer(
21 timedelta(days=2, hours=2, seconds=5))
22 self.assertEquals(
23 '2 days, 2:00:05',
24 serializer.toDataForJSON('application/json'))
25
26 def test_timedelta_users_StrJSONSerializer(self):
27 delta = timedelta(seconds=5)
28 serializer = IJSONPublishable(delta)
29 self.assertEquals('0:00:05',
30 serializer.toDataForJSON('application/json'))
031
=== modified file 'lib/lp/services/webservice/wadl-to-refhtml.xsl'
--- lib/lp/services/webservice/wadl-to-refhtml.xsl 2011-01-13 19:45:32 +0000
+++ lib/lp/services/webservice/wadl-to-refhtml.xsl 2011-07-21 19:34:45 +0000
@@ -165,9 +165,16 @@
165 <xsl:call-template name="resource-uri-doc">165 <xsl:call-template name="resource-uri-doc">
166 <xsl:with-param name="url">166 <xsl:with-param name="url">
167 <xsl:choose>167 <xsl:choose>
168 <xsl:when test="@id = 'has_milestones'168 <xsl:when test="
169 or @id = 'bug_target'169 @id = 'bug_link_target'
170 or @id = 'has_bugs'">170 or @id = 'bug_target'
171 or @id = 'has_bugs'
172 or @id = 'has_milestones'
173 or @id = 'object_with_translation_imports'
174 or @id = 'question_target'
175 or @id = 'specification_target'
176 or @id = 'structural_subscription_target'
177 ">
171 <em>depends on the underlying entry</em>178 <em>depends on the underlying entry</em>
172 </xsl:when>179 </xsl:when>
173 <xsl:otherwise>180 <xsl:otherwise>
@@ -264,10 +271,18 @@
264 <xsl:text>/+build/</xsl:text>271 <xsl:text>/+build/</xsl:text>
265 <var>&lt;id&gt;</var>272 <var>&lt;id&gt;</var>
266 </xsl:when>273 </xsl:when>
274 <xsl:when test="@id = 'builder'">
275 <xsl:text>/builders/</xsl:text>
276 <var>&lt;builder.name&gt;</var>
277 </xsl:when>
267 <xsl:when test="@id = 'cve'">278 <xsl:when test="@id = 'cve'">
268 <xsl:text>/bugs/cve/</xsl:text>279 <xsl:text>/bugs/cve/</xsl:text>
269 <var>&lt;sequence&gt;</var>280 <var>&lt;sequence&gt;</var>
270 </xsl:when>281 </xsl:when>
282 <xsl:when test="@id = 'country'">
283 <xsl:text>/+countries/</xsl:text>
284 <var>&lt;iso3166code2&gt;</var>
285 </xsl:when>
271 <xsl:when test="@id = 'distribution_source_package'">286 <xsl:when test="@id = 'distribution_source_package'">
272 <xsl:text>/</xsl:text>287 <xsl:text>/</xsl:text>
273 <var>&lt;distribution.name&gt;</var>288 <var>&lt;distribution.name&gt;</var>
@@ -294,6 +309,15 @@
294 <xsl:text>/+email/</xsl:text>309 <xsl:text>/+email/</xsl:text>
295 <var>&lt;email&gt;</var>310 <var>&lt;email&gt;</var>
296 </xsl:when>311 </xsl:when>
312 <xsl:when test="@id = 'gpg_key'">
313 <xsl:text>/</xsl:text>
314 <var>&lt;person.name&gt;</var>
315 <xsl:text>/+gpg-keys/</xsl:text>
316 <var>&lt;keyid&gt;</var>
317 </xsl:when>
318 <xsl:when test="@id = 'hwdb'">
319 <xsl:text>/+hwdb</xsl:text>
320 </xsl:when>
297 <xsl:when test="@id = 'h_w_device'">321 <xsl:when test="@id = 'h_w_device'">
298 <xsl:text>/+hwdb/+device/</xsl:text>322 <xsl:text>/+hwdb/+device/</xsl:text>
299 <var>&lt;id&gt;</var>323 <var>&lt;id&gt;</var>
@@ -360,6 +384,17 @@
360 <xsl:text>/~</xsl:text>384 <xsl:text>/~</xsl:text>
361 <var>&lt;name&gt;</var>385 <var>&lt;name&gt;</var>
362 </xsl:when>386 </xsl:when>
387 <xsl:when test="@id = 'pillars'">
388 <xsl:text>/pillars</xsl:text>
389 </xsl:when>
390 <xsl:when test="@id = 'processor'">
391 <xsl:text>/+processors/</xsl:text>
392 <var>&lt;processor.name&gt;</var>
393 </xsl:when>
394 <xsl:when test="@id = 'processor_family'">
395 <xsl:text>/+processor-families/</xsl:text>
396 <var>&lt;processor_family.name&gt;</var>
397 </xsl:when>
363 <xsl:when test="@id = 'product_release'">398 <xsl:when test="@id = 'product_release'">
364 <xsl:text>/</xsl:text>399 <xsl:text>/</xsl:text>
365 <var>&lt;product.name&gt;</var>400 <var>&lt;product.name&gt;</var>
@@ -398,6 +433,20 @@
398 <xsl:text>/</xsl:text>433 <xsl:text>/</xsl:text>
399 <var>&lt;name&gt;</var>434 <var>&lt;name&gt;</var>
400 </xsl:when>435 </xsl:when>
436 <xsl:when test="@id = 'question'">
437 <xsl:text>/</xsl:text>
438 <var>&lt;target.name&gt;</var>
439 <xsl:text>/+question/</xsl:text>
440 <var >&lt;question.id&gt;</var>
441 </xsl:when>
442 <xsl:when test="@id = 'question_message'">
443 <xsl:text>/</xsl:text>
444 <var>&lt;target.name&gt;</var>
445 <xsl:text>/+question/</xsl:text>
446 <var >&lt;question.id&gt;</var>
447 <xsl:text>/messages/</xsl:text>
448 <var >&lt;message.index&gt;</var>
449 </xsl:when>
401 <xsl:when test="@id = 'source_package'">450 <xsl:when test="@id = 'source_package'">
402 <xsl:text>/</xsl:text>451 <xsl:text>/</xsl:text>
403 <var>&lt;distribution.name&gt;</var>452 <var>&lt;distribution.name&gt;</var>
@@ -414,6 +463,34 @@
414 <xsl:text>/+sourcepub/</xsl:text>463 <xsl:text>/+sourcepub/</xsl:text>
415 <var>&lt;id&gt;</var>464 <var>&lt;id&gt;</var>
416 </xsl:when>465 </xsl:when>
466 <xsl:when test="@id = 'specification'">
467 <xsl:text>/</xsl:text>
468 <var>&lt;target.name&gt;</var>
469 <xsl:text>/+spec/</xsl:text>
470 <var >&lt;specification.name&gt;</var>
471 </xsl:when>
472 <xsl:when test="@id = 'specification_branch'">
473 <xsl:text>/</xsl:text>
474 <var>&lt;target.name&gt;</var>
475 <xsl:text>/+spec/</xsl:text>
476 <var >&lt;specification.name&gt;</var>
477 <xsl:text>/+branch/</xsl:text>
478 <var >&lt;branch.unique_name[1:]&gt;</var>
479 </xsl:when>
480 <xsl:when test="@id = 'specification_subscription'">
481 <xsl:text>/</xsl:text>
482 <var>&lt;target.name&gt;</var>
483 <xsl:text>/+spec/</xsl:text>
484 <var >&lt;specification.name&gt;</var>
485 <xsl:text>/+subscription/</xsl:text>
486 <var >&lt;person.name&gt;</var>
487 </xsl:when>
488 <xsl:when test="@id = 'ssh_key'">
489 <xsl:text>/</xsl:text>
490 <var>&lt;person.name&gt;</var>
491 <xsl:text>/+ssh-keys/</xsl:text>
492 <var>&lt;keyid&gt;</var>
493 </xsl:when>
417 <xsl:when test="@id = 'team_membership'">494 <xsl:when test="@id = 'team_membership'">
418 <xsl:text>/~</xsl:text>495 <xsl:text>/~</xsl:text>
419 <var>&lt;team.name&gt;</var>496 <var>&lt;team.name&gt;</var>
@@ -864,6 +941,12 @@
864 <xsl:apply-templates select="wadl:doc"/>941 <xsl:apply-templates select="wadl:doc"/>
865 <xsl:apply-templates select="wadl:request"/>942 <xsl:apply-templates select="wadl:request"/>
866 <xsl:apply-templates select="wadl:response"/>943 <xsl:apply-templates select="wadl:response"/>
944 <xsl:if test="not(wadl:response)">
945 <xsl:apply-templates select="wadl:doc//html:th[
946 node() = 'return:'
947 ]"/>
948 </xsl:if>
949 <xsl:call-template name="error-documentation"/>
867 </xsl:when>950 </xsl:when>
868 <xsl:otherwise>951 <xsl:otherwise>
869 <p><em>Missing documentation.</em></p>952 <p><em>Missing documentation.</em></p>
@@ -910,6 +993,39 @@
910 </p>993 </p>
911 </xsl:template>994 </xsl:template>
912995
996 <!-- Documentation of the custom method return type. -->
997 <xsl:template match="wadl:doc//html:th[node() = 'return:']">
998 <h6>Response (application/json)</h6>
999 <xsl:choose>
1000 <xsl:when test="following-sibling::html:td/text()">
1001 <p><xsl:apply-templates select="following-sibling::html:td"
1002 mode="copy"/></p>
1003 </xsl:when>
1004 <xsl:otherwise>
1005 <xsl:apply-templates select="following-sibling::html:td"
1006 mode="copy"/>
1007 </xsl:otherwise>
1008 </xsl:choose>
1009 </xsl:template>
1010
1011 <!-- Documentation of the error raised by the operation. -->
1012 <xsl:template name="error-documentation">
1013 <xsl:if test="wadl:doc//html:th[node() = 'raise:']">
1014 <h6>Errors</h6>
1015 <ul>
1016 <xsl:apply-templates
1017 select="wadl:doc//html:th[node() = 'raise:']"/>
1018 </ul>
1019 </xsl:if>
1020 </xsl:template>
1021
1022 <xsl:template match="wadl:doc//html:th[node() = 'raise:']">
1023 <li>
1024 <xsl:apply-templates select="following-sibling::html:td"
1025 mode="copy"/>
1026 </li>
1027 </xsl:template>
1028
913 <!-- Documentation for request parameter. -->1029 <!-- Documentation for request parameter. -->
914 <xsl:template match="wadl:param">1030 <xsl:template match="wadl:param">
915 <tr>1031 <tr>
@@ -1032,11 +1148,15 @@
1032 mode="representation-type"/>1148 mode="representation-type"/>
1033 </xsl:template>1149 </xsl:template>
10341150
1035 <!-- Omit docutils parameter lists in methods since they are redundant1151 <!-- Omit docutils parameter table in methods. The parameter names
1036 or misleading with the one we give. -->1152 description is either redundant or misleading with the one we
1153 give.
1154
1155 We process the return and raise parameters separately.
1156 -->
1037 <xsl:template match="wadl:method//html:table[1157 <xsl:template match="wadl:method//html:table[
1038 contains(@class, 'field-list')]"1158 contains(@class, 'field-list')]"
1039 mode="copy"/>1159 mode="copy" />
10401160
1041 <!-- Output the mediaType attribute of a representation -->1161 <!-- Output the mediaType attribute of a representation -->
1042 <xsl:template match="wadl:representation[@mediaType]"1162 <xsl:template match="wadl:representation[@mediaType]"
10431163
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2011-06-23 13:42:38 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2011-07-21 19:34:45 +0000
@@ -32,18 +32,20 @@
32 path_expression="string:+binarypub"32 path_expression="string:+binarypub"
33 attribute_to_parent="archive"33 attribute_to_parent="archive"
34 urldata="lp.soyuz.browser.publishing.BinaryPublicationURL"/>34 urldata="lp.soyuz.browser.publishing.BinaryPublicationURL"/>
35 <browser:url35 <browser:url for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
36 for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
37 path_expression="string:+processor-families"36 path_expression="string:+processor-families"
38 parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>37 parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
39 <browser:url38 <browser:url
40 for="lp.soyuz.interfaces.processor.IProcessorFamily"39 for="lp.soyuz.interfaces.processor.IProcessorFamily"
41 path_expression="string:${name}"40 path_expression="string:${name}"
42 parent_utility="lp.soyuz.interfaces.processor.IProcessorFamilySet" />41 parent_utility="lp.soyuz.interfaces.processor.IProcessorFamilySet" />
42 <browser:url for="lp.soyuz.interfaces.processor.IProcessorSet"
43 path_expression="string:+processors"
44 parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
43 <browser:url45 <browser:url
44 for="lp.soyuz.interfaces.processor.IProcessor"46 for="lp.soyuz.interfaces.processor.IProcessor"
45 path_expression="string:${id}"47 path_expression="string:${name}"
46 attribute_to_parent="family" />48 parent_utility="lp.soyuz.interfaces.processor.IProcessorSet" />
47 </facet>49 </facet>
48 <browser:navigation50 <browser:navigation
49 module="lp.soyuz.browser.binarypackagerelease"51 module="lp.soyuz.browser.binarypackagerelease"
@@ -233,7 +235,7 @@
233 <browser:navigation235 <browser:navigation
234 module="lp.soyuz.browser.processor"236 module="lp.soyuz.browser.processor"
235 classes="237 classes="
236 ProcessorFamilySetNavigation ProcessorFamilyNavigation"/>238 ProcessorFamilySetNavigation ProcessorSetNavigation"/>
237 <browser:url239 <browser:url
238 for="lp.soyuz.interfaces.archive.IPPA"240 for="lp.soyuz.interfaces.archive.IPPA"
239 path_expression="string:+archive"241 path_expression="string:+archive"
240242
=== modified file 'lib/lp/soyuz/browser/processor.py'
--- lib/lp/soyuz/browser/processor.py 2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/browser/processor.py 2011-07-21 19:34:45 +0000
@@ -8,15 +8,15 @@
88
9__all__ = [9__all__ = [
10 'ProcessorFamilySetNavigation',10 'ProcessorFamilySetNavigation',
11 'ProcessorFamilyNavigation',11 'ProcessorSetNavigation',
12 ]12 ]
1313
1414
15from canonical.launchpad.webapp import Navigation15from canonical.launchpad.webapp import Navigation
16from lp.app.errors import NotFoundError16from lp.app.errors import NotFoundError
17from lp.soyuz.interfaces.processor import (17from lp.soyuz.interfaces.processor import (
18 IProcessorFamily,
19 IProcessorFamilySet,18 IProcessorFamilySet,
19 IProcessorSet,
20 )20 )
2121
2222
@@ -32,15 +32,9 @@
32 return family32 return family
3333
3434
35class ProcessorFamilyNavigation(Navigation):35class ProcessorSetNavigation(Navigation):
36 """IProcessorFamily navigation."""36 """IProcessorFamilySet navigation."""
3737 usedfor = IProcessorSet
38 usedfor = IProcessorFamily38
3939 def traverse(self, name):
40 def traverse(self, id_):40 return self.context.getByName(name)
41 id_ = int(id_)
42 processors = self.processors
43 for p in processors:
44 if p.id == id_:
45 return p
46 raise NotFoundError(id_)
4741
=== added file 'lib/lp/soyuz/browser/tests/test_processor.py'
--- lib/lp/soyuz/browser/tests/test_processor.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/tests/test_processor.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,41 @@
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 process navigation."""
5
6__metaclass__ = type
7
8from canonical.testing.layers import DatabaseFunctionalLayer
9from canonical.launchpad.webapp.publisher import canonical_url
10from lp.testing import TestCaseWithFactory
11from lp.testing.publication import test_traverse
12
13
14class TestProcessorNavigation(TestCaseWithFactory):
15 layer = DatabaseFunctionalLayer
16
17 def test_processor_family_url(self):
18 family = self.factory.makeProcessorFamily('quantum')
19 self.assertEquals(
20 '/+processor-families/quantum',
21 canonical_url(family, force_local_path=True))
22
23 def test_processor_url(self):
24 family = self.factory.makeProcessorFamily('quantum')
25 quantum = family.processors[0]
26 self.assertEquals(
27 '/+processors/quantum',
28 canonical_url(quantum, force_local_path=True))
29
30 def test_processor_family_navigation(self):
31 family = self.factory.makeProcessorFamily('quantum')
32 obj, view, request = test_traverse(
33 'http://api.launchpad.dev/devel/+processor-families/quantum')
34 self.assertEquals(family, obj)
35
36 def test_processor_navigation(self):
37 family = self.factory.makeProcessorFamily('quantum')
38 obj, view, request = test_traverse(
39 'http://api.launchpad.dev/'
40 'devel/+processors/quantum')
41 self.assertEquals(family.processors[0], obj)
042
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2011-07-12 14:23:40 +0000
+++ lib/lp/soyuz/configure.zcml 2011-07-21 19:34:45 +0000
@@ -11,6 +11,7 @@
11 i18n_domain="launchpad">11 i18n_domain="launchpad">
12 <include12 <include
13 package=".browser"/>13 package=".browser"/>
14 <authorizations module=".security" />
1415
15 <!-- PackageCloner -->16 <!-- PackageCloner -->
1617
@@ -375,6 +376,12 @@
375 <allow376 <allow
376 interface="lp.soyuz.interfaces.processor.IProcessorFamilySet"/>377 interface="lp.soyuz.interfaces.processor.IProcessorFamilySet"/>
377 </securedutility>378 </securedutility>
379 <securedutility
380 class="lp.soyuz.model.processor.ProcessorSet"
381 provides="lp.soyuz.interfaces.processor.IProcessorSet">
382 <allow
383 interface="lp.soyuz.interfaces.processor.IProcessorSet"/>
384 </securedutility>
378 <adapter385 <adapter
379 for="lp.soyuz.interfaces.distroarchseriesbinarypackagerelease.IDistroArchSeriesBinaryPackageRelease"386 for="lp.soyuz.interfaces.distroarchseriesbinarypackagerelease.IDistroArchSeriesBinaryPackageRelease"
380 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"387 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
381388
=== modified file 'lib/lp/soyuz/interfaces/processor.py'
--- lib/lp/soyuz/interfaces/processor.py 2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/interfaces/processor.py 2011-07-21 19:34:45 +0000
@@ -11,6 +11,8 @@
11 'IProcessor',11 'IProcessor',
12 'IProcessorFamily',12 'IProcessorFamily',
13 'IProcessorFamilySet',13 'IProcessorFamilySet',
14 'IProcessorSet',
15 'ProcessorNotFound',
14 ]16 ]
1517
16from zope.interface import (18from zope.interface import (
@@ -38,6 +40,12 @@
38 CollectionField,40 CollectionField,
39 Reference,41 Reference,
40 )42 )
43from lp.app.errors import NameLookupFailed
44
45
46class ProcessorNotFound(NameLookupFailed):
47 """Exception raised when a processor name isn't found."""
48 _message_prefix = 'No such processor'
4149
4250
43class IProcessor(Interface):51class IProcessor(Interface):
@@ -49,7 +57,7 @@
49 # the WADL generation work it must be back-dated to the earliest version.57 # the WADL generation work it must be back-dated to the earliest version.
50 # Note that individual attributes and methods can and must truthfully set58 # Note that individual attributes and methods can and must truthfully set
51 # 'devel' as their version.59 # 'devel' as their version.
52 export_as_webservice_entry(publish_web_link=True, as_of='beta')60 export_as_webservice_entry(publish_web_link=False, as_of='beta')
53 id = Attribute("The Processor ID")61 id = Attribute("The Processor ID")
54 family = exported(62 family = exported(
55 Reference(63 Reference(
@@ -84,7 +92,7 @@
84 # 'devel' as their version.92 # 'devel' as their version.
85 export_as_webservice_entry(93 export_as_webservice_entry(
86 plural_name='processor_families',94 plural_name='processor_families',
87 publish_web_link=True,95 publish_web_link=False,
88 as_of='beta')96 as_of='beta')
8997
90 id = Attribute("The ProcessorFamily ID")98 id = Attribute("The ProcessorFamily ID")
@@ -123,14 +131,36 @@
123 """131 """
124132
125133
134class IProcessorSet(Interface):
135 """Operations related to Processor instances."""
136 export_as_webservice_collection(IProcessor)
137
138 @operation_parameters(
139 name=TextLine(required=True))
140 @operation_returns_entry(IProcessor)
141 @export_read_operation()
142 @operation_for_version('devel')
143 def getByName(name):
144 """Return the IProcessor instance with the matching name.
145
146 :param name: The name to look for.
147 :raise ProcessorNotFound: if there is no processor with that name.
148 :return: A `IProcessor` instance if found
149 """
150
151 @collection_default_content()
152 def getAll():
153 """Return all the `IProcessor` known to Launchpad."""
154
155
126class IProcessorFamilySet(Interface):156class IProcessorFamilySet(Interface):
127 """Operations related to ProcessorFamily instances."""157 """Operations related to ProcessorFamily instances."""
128158
129 export_as_webservice_collection(Interface)159 export_as_webservice_collection(IProcessorFamily)
130160
131 @operation_parameters(161 @operation_parameters(
132 name=TextLine(required=True))162 name=TextLine(required=True))
133 @operation_returns_entry(Interface)163 @operation_returns_entry(IProcessorFamily)
134 @export_read_operation()164 @export_read_operation()
135 @operation_for_version('devel')165 @operation_for_version('devel')
136 def getByName(name):166 def getByName(name):
137167
=== modified file 'lib/lp/soyuz/interfaces/webservice.py'
--- lib/lp/soyuz/interfaces/webservice.py 2011-06-23 18:26:26 +0000
+++ lib/lp/soyuz/interfaces/webservice.py 2011-07-21 19:34:45 +0000
@@ -35,6 +35,7 @@
35 'IProcessor',35 'IProcessor',
36 'IProcessorFamily',36 'IProcessorFamily',
37 'IProcessorFamilySet',37 'IProcessorFamilySet',
38 'IProcessorSet',
38 'ISourcePackagePublishingHistory',39 'ISourcePackagePublishingHistory',
39 'IncompatibleArguments',40 'IncompatibleArguments',
40 'InsufficientUploadRights',41 'InsufficientUploadRights',
@@ -96,6 +97,7 @@
96 IProcessor,97 IProcessor,
97 IProcessorFamily,98 IProcessorFamily,
98 IProcessorFamilySet,99 IProcessorFamilySet,
100 IProcessorSet,
99 )101 )
100from lp.soyuz.interfaces.publishing import (102from lp.soyuz.interfaces.publishing import (
101 IBinaryPackagePublishingHistory,103 IBinaryPackagePublishingHistory,
@@ -105,7 +107,6 @@
105107
106from canonical.launchpad.components.apihelpers import (108from canonical.launchpad.components.apihelpers import (
107 patch_collection_property,109 patch_collection_property,
108 patch_entry_return_type,
109 patch_plain_parameter_type,110 patch_plain_parameter_type,
110 patch_reference_property,111 patch_reference_property,
111 )112 )
@@ -115,15 +116,10 @@
115from canonical.launchpad.interfaces import _schema_circular_imports116from canonical.launchpad.interfaces import _schema_circular_imports
116_schema_circular_imports117_schema_circular_imports
117118
118from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
119IProcessorFamilySet.queryTaggedValue(
120 LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IProcessorFamily
121
122# IProcessor119# IProcessor
123patch_reference_property(120patch_reference_property(
124 IProcessor, 'family', IProcessorFamily)121 IProcessor, 'family', IProcessorFamily)
125122
126patch_entry_return_type(IProcessorFamilySet, 'getByName', IProcessorFamily)
127patch_collection_property(123patch_collection_property(
128 IArchive, 'enabled_restricted_families', IProcessorFamily)124 IArchive, 'enabled_restricted_families', IProcessorFamily)
129patch_plain_parameter_type(125patch_plain_parameter_type(
130126
=== modified file 'lib/lp/soyuz/model/processor.py'
--- lib/lp/soyuz/model/processor.py 2010-11-12 02:15:28 +0000
+++ lib/lp/soyuz/model/processor.py 2011-07-21 19:34:45 +0000
@@ -25,6 +25,8 @@
25 IProcessor,25 IProcessor,
26 IProcessorFamily,26 IProcessorFamily,
27 IProcessorFamilySet,27 IProcessorFamilySet,
28 IProcessorSet,
29 ProcessorNotFound,
28 )30 )
2931
3032
@@ -42,6 +44,24 @@
42 return "<Processor %r>" % self.title44 return "<Processor %r>" % self.title
4345
4446
47class ProcessorSet:
48 """See `IProcessorSet`."""
49 implements(IProcessorSet)
50
51 def getByName(self, name):
52 """See `IProcessorSet`."""
53 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
54 processor = store.find(Processor, Processor.name == name).one()
55 if processor is None:
56 raise ProcessorNotFound(name)
57 return processor
58
59 def getAll(self):
60 """See `IProcessorSet`."""
61 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
62 return store.find(Processor)
63
64
45class ProcessorFamily(SQLBase):65class ProcessorFamily(SQLBase):
46 implements(IProcessorFamily)66 implements(IProcessorFamily)
47 _table = 'ProcessorFamily'67 _table = 'ProcessorFamily'
@@ -64,6 +84,7 @@
6484
65class ProcessorFamilySet:85class ProcessorFamilySet:
66 implements(IProcessorFamilySet)86 implements(IProcessorFamilySet)
87
67 def getByName(self, name):88 def getByName(self, name):
68 """Please see `IProcessorFamilySet`."""89 """Please see `IProcessorFamilySet`."""
69 # Please note that ProcessorFamily.name is unique i.e. the database90 # Please note that ProcessorFamily.name is unique i.e. the database
7091
=== added file 'lib/lp/soyuz/security.py'
--- lib/lp/soyuz/security.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/security.py 2011-07-21 19:34:45 +0000
@@ -0,0 +1,26 @@
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"""Security adapters for the soyuz module."""
5
6__metaclass__ = type
7__all__ = [
8 'ViewProcessor',
9 'ViewProcessorFamily',
10 ]
11
12from lp.app.security import AnonymousAuthorization
13from lp.soyuz.interfaces.processor import (
14 IProcessor,
15 IProcessorFamily,
16 )
17
18
19class ViewProcessor(AnonymousAuthorization):
20 """Anyone can view an `IProcessor`."""
21 usedfor = IProcessor
22
23
24class ViewProcessorFamily(AnonymousAuthorization):
25 """Anyone can view an `IProcessorFamily`."""
26 usedfor = IProcessorFamily
027
=== modified file 'lib/lp/soyuz/tests/test_packagecloner.py'
--- lib/lp/soyuz/tests/test_packagecloner.py 2011-06-23 09:31:34 +0000
+++ lib/lp/soyuz/tests/test_packagecloner.py 2011-07-21 19:34:45 +0000
@@ -344,7 +344,6 @@
344 # This is a processor family without a DAS in the source, so344 # This is a processor family without a DAS in the source, so
345 # we expect no builds.345 # we expect no builds.
346 family = self.factory.makeProcessorFamily(name="armel")346 family = self.factory.makeProcessorFamily(name="armel")
347 self.factory.makeProcessor(family=family, name="armel")
348 proc_families = [family]347 proc_families = [family]
349 copy_archive, distroseries = self.makeCopyArchive(348 copy_archive, distroseries = self.makeCopyArchive(
350 [package_info], proc_families=proc_families)349 [package_info], proc_families=proc_families)
@@ -357,7 +356,6 @@
357 # One of these processor families has a DAS in the source, so356 # One of these processor families has a DAS in the source, so
358 # we expect one set of builds357 # we expect one set of builds
359 family = self.factory.makeProcessorFamily(name="armel")358 family = self.factory.makeProcessorFamily(name="armel")
360 self.factory.makeProcessor(family=family, name="armel")
361 proc_families = [family, ProcessorFamilySet().getByName("x86")]359 proc_families = [family, ProcessorFamilySet().getByName("x86")]
362 copy_archive, distroseries = self.makeCopyArchive(360 copy_archive, distroseries = self.makeCopyArchive(
363 [package_info], proc_families=proc_families)361 [package_info], proc_families=proc_families)
@@ -399,7 +397,6 @@
399 copy_archive, distroseries, proc_families=proc_families)397 copy_archive, distroseries, proc_families=proc_families)
400 self.checkBuilds(copy_archive, [package_info, package_info])398 self.checkBuilds(copy_archive, [package_info, package_info])
401399
402
403 def diffArchives(self, target_archive, target_distroseries,400 def diffArchives(self, target_archive, target_distroseries,
404 source_archive=None, source_distroseries=None):401 source_archive=None, source_distroseries=None):
405 """Run a packageSetDiff of two archives."""402 """Run a packageSetDiff of two archives."""
@@ -423,6 +420,7 @@
423 expected_changed_tuples = [(e.name, e.version)420 expected_changed_tuples = [(e.name, e.version)
424 for e in expected_changed]421 for e in expected_changed]
425 expected_new_tuples = [(e.name, e.version) for e in expected_new]422 expected_new_tuples = [(e.name, e.version) for e in expected_new]
423
426 def get_tuples(source_keys):424 def get_tuples(source_keys):
427 tuples = []425 tuples = []
428 for source_key in source_keys:426 for source_key in source_keys:
@@ -503,7 +501,6 @@
503 [package_infos[0]], [package_infos[1]], diff,501 [package_infos[0]], [package_infos[1]], diff,
504 distroseries.distribution.main_archive)502 distroseries.distribution.main_archive)
505503
506
507 def mergeCopy(self, target_archive, target_distroseries,504 def mergeCopy(self, target_archive, target_distroseries,
508 source_archive=None, source_distroseries=None):505 source_archive=None, source_distroseries=None):
509 if source_distroseries is None:506 if source_distroseries is None:
510507
=== modified file 'lib/lp/soyuz/tests/test_processor.py'
--- lib/lp/soyuz/tests/test_processor.py 2010-10-04 19:50:45 +0000
+++ lib/lp/soyuz/tests/test_processor.py 2011-07-21 19:34:45 +0000
@@ -5,13 +5,29 @@
55
6from zope.component import getUtility6from zope.component import getUtility
77
8from canonical.testing.layers import LaunchpadZopelessLayer8from canonical.launchpad.webapp.interfaces import (
9 DEFAULT_FLAVOR,
10 IStoreSelector,
11 MAIN_STORE,
12 )
13from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
14from canonical.testing.layers import (
15 DatabaseFunctionalLayer,
16 LaunchpadZopelessLayer,
17 )
18
9from lp.soyuz.interfaces.processor import (19from lp.soyuz.interfaces.processor import (
10 IProcessor,20 IProcessor,
11 IProcessorFamily,21 IProcessorFamily,
12 IProcessorFamilySet,22 IProcessorFamilySet,
13 )23 IProcessorSet,
14from lp.testing import TestCaseWithFactory24 ProcessorNotFound,
25 )
26from lp.testing import (
27 ExpectedException,
28 logout,
29 TestCaseWithFactory,
30 )
1531
1632
17class ProcessorFamilyTests(TestCaseWithFactory):33class ProcessorFamilyTests(TestCaseWithFactory):
@@ -29,16 +45,83 @@
29 """Test adding a new Processor to a ProcessorFamily."""45 """Test adding a new Processor to a ProcessorFamily."""
30 family = getUtility(IProcessorFamilySet).new("avr", "Atmel AVR",46 family = getUtility(IProcessorFamilySet).new("avr", "Atmel AVR",
31 "The Modified Harvard architecture 8-bit RISC processors.")47 "The Modified Harvard architecture 8-bit RISC processors.")
32 proc = family.addProcessor("avr2001", "The 2001 AVR", "Fast as light.")48 proc = family.addProcessor(
49 "avr2001", "The 2001 AVR", "Fast as light.")
33 self.assertProvides(proc, IProcessor)50 self.assertProvides(proc, IProcessor)
34 self.assertEquals(family, proc.family)51 self.assertEquals(family, proc.family)
3552
36 def test_get_restricted(self):53 def test_get_restricted(self):
37 """Test retrieving all restricted processors."""54 """Test retrieving all restricted processors."""
38 family_set = getUtility(IProcessorFamilySet)55 family_set = getUtility(IProcessorFamilySet)
39 normal_family = getUtility(IProcessorFamilySet).new("avr", "Atmel AVR",56 normal_family = getUtility(IProcessorFamilySet).new(
57 "avr", "Atmel AVR",
40 "The Modified Harvard architecture 8-bit RISC processors.")58 "The Modified Harvard architecture 8-bit RISC processors.")
41 restricted_family = getUtility(IProcessorFamilySet).new("5051", "5051",59 restricted_family = getUtility(IProcessorFamilySet).new(
42 "Another small processor family", restricted=True)60 "5051", "5051", "Another small processor family",
61 restricted=True)
43 self.assertFalse(normal_family in family_set.getRestricted())62 self.assertFalse(normal_family in family_set.getRestricted())
44 self.assertTrue(restricted_family in family_set.getRestricted())63 self.assertTrue(restricted_family in family_set.getRestricted())
64
65
66class ProcessorSetTests(TestCaseWithFactory):
67 layer = DatabaseFunctionalLayer
68
69 def test_getByName(self):
70 processor_set = getUtility(IProcessorSet)
71 q1 = self.factory.makeProcessorFamily(name='q1')
72 self.assertEquals(q1.processors[0], processor_set.getByName('q1'))
73
74 def test_getByName_not_found(self):
75 processor_set = getUtility(IProcessorSet)
76 with ExpectedException(ProcessorNotFound, 'No such processor.*'):
77 processor_set.getByName('q1')
78
79 def test_getAll(self):
80 processor_set = getUtility(IProcessorSet)
81 # Make it easy to filter out sample data
82 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
83 store.execute("UPDATE Processor SET name = 'sample_data_' || name")
84 self.factory.makeProcessorFamily(name='q1')
85 self.factory.makeProcessorFamily(name='i686')
86 self.factory.makeProcessorFamily(name='g4')
87 self.assertEquals(
88 ['g4', 'i686', 'q1'],
89 sorted(
90 processor.name for processor in processor_set.getAll()
91 if not processor.name.startswith('sample_data_')))
92
93
94class ProcessorSetWebServiceTests(TestCaseWithFactory):
95 layer = DatabaseFunctionalLayer
96
97 def setUp(self):
98 super(ProcessorSetWebServiceTests, self).setUp()
99 self.webservice = LaunchpadWebServiceCaller()
100
101 def test_getByName(self):
102 self.factory.makeProcessorFamily(name='transmeta')
103 logout()
104
105 processor = self.webservice.named_get(
106 '/+processors', 'getByName', name='transmeta',
107 api_version='devel',
108 ).jsonBody()
109 self.assertEquals('transmeta', processor['name'])
110
111 def test_default_collection(self):
112 # Make it easy to filter out sample data
113 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
114 store.execute("UPDATE Processor SET name = 'sample_data_' || name")
115 self.factory.makeProcessorFamily(name='q1')
116 self.factory.makeProcessorFamily(name='i686')
117 self.factory.makeProcessorFamily(name='g4')
118
119 logout()
120
121 collection = self.webservice.get(
122 '/+processors?ws.size=10', api_version='devel').jsonBody()
123 self.assertEquals(
124 ['g4', 'i686', 'q1'],
125 sorted(
126 processor['name'] for processor in collection['entries']
127 if not processor['name'].startswith('sample_data_')))
45128
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-07-18 14:07:48 +0000
+++ lib/lp/testing/__init__.py 2011-07-21 19:34:45 +0000
@@ -618,6 +618,17 @@
618 self._unfoldEmailHeader(expected),618 self._unfoldEmailHeader(expected),
619 self._unfoldEmailHeader(observed))619 self._unfoldEmailHeader(observed))
620620
621 def assertStartsWith(self, s, prefix):
622 if not s.startswith(prefix):
623 raise AssertionError(
624 'string %r does not start with %r' % (s, prefix))
625
626 def assertEndsWith(self, s, suffix):
627 """Asserts that s ends with suffix."""
628 if not s.endswith(suffix):
629 raise AssertionError(
630 'string %r does not end with %r' % (s, suffix))
631
621632
622class TestCaseWithFactory(TestCase):633class TestCaseWithFactory(TestCase):
623634
624635
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-07-19 13:51:36 +0000
+++ lib/lp/testing/factory.py 2011-07-21 19:34:45 +0000
@@ -894,6 +894,9 @@
894 restricted=False):894 restricted=False):
895 """Create a new processor family.895 """Create a new processor family.
896896
897 A default processor for the family will be created with the
898 same name as the family.
899
897 :param name: Name of the family (e.g. x86)900 :param name: Name of the family (e.g. x86)
898 :param title: Optional title of the family901 :param title: Optional title of the family
899 :param description: Optional extended description902 :param description: Optional extended description
@@ -906,11 +909,11 @@
906 description = "Description of the %s processor family" % name909 description = "Description of the %s processor family" % name
907 if title is None:910 if title is None:
908 title = "%s and compatible processors." % name911 title = "%s and compatible processors." % name
909 family = getUtility(IProcessorFamilySet).new(name, title, description,912 family = getUtility(IProcessorFamilySet).new(
910 restricted=restricted)913 name, title, description, restricted=restricted)
911 # Make sure there's at least one processor in the family, so that914 # Make sure there's at least one processor in the family, so that
912 # other things can have a default processor.915 # other things can have a default processor.
913 self.makeProcessor(family=family)916 self.makeProcessor(name=name, family=family)
914 return family917 return family
915918
916 def makeProductRelease(self, milestone=None, product=None,919 def makeProductRelease(self, milestone=None, product=None,
917920
=== modified file 'lib/lp/testing/publication.py'
--- lib/lp/testing/publication.py 2010-10-03 15:30:06 +0000
+++ lib/lp/testing/publication.py 2011-07-21 19:34:45 +0000
@@ -20,6 +20,7 @@
20from zope.interface import providedBy20from zope.interface import providedBy
21from zope.publisher.interfaces.browser import IDefaultSkin21from zope.publisher.interfaces.browser import IDefaultSkin
22from zope.security.management import restoreInteraction22from zope.security.management import restoreInteraction
23from zope.security.proxy import removeSecurityProxy
2324
24from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag25from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag
25import canonical.launchpad.layers as layers26import canonical.launchpad.layers as layers
@@ -30,6 +31,7 @@
30 )31 )
31from canonical.launchpad.webapp.servers import ProtocolErrorPublication32from canonical.launchpad.webapp.servers import ProtocolErrorPublication
3233
34
33# Defines an helper function that returns the appropriate35# Defines an helper function that returns the appropriate
34# IRequest and IPublication.36# IRequest and IPublication.
35def get_request_and_publication(host='localhost', port=None,37def get_request_and_publication(host='localhost', port=None,
@@ -119,9 +121,15 @@
119 getUtility(IOpenLaunchBag).clear()121 getUtility(IOpenLaunchBag).clear()
120 app = publication.getApplication(request)122 app = publication.getApplication(request)
121 view = request.traverse(app)123 view = request.traverse(app)
122 # Since the last traversed object is the view, the second last should be124 # Find the object from the view instead on relying that it stays
123 # the object that the view is on.125 # in the traversed_objects stack. That doesn't apply to the web
124 obj = request.traversed_objects[-2]126 # service for example.
127 try:
128 obj = removeSecurityProxy(view).context
129 except AttributeError:
130 # But sometime the view didn't store the context...
131 # Use the last traversed object in these cases.
132 obj = request.traversed_objects[-2]
125133
126 restoreInteraction()134 restoreInteraction()
127135
128136
=== modified file 'lib/lp/testing/tests/test_publication.py'
--- lib/lp/testing/tests/test_publication.py 2010-10-04 19:50:45 +0000
+++ lib/lp/testing/tests/test_publication.py 2011-07-21 19:34:45 +0000
@@ -23,6 +23,7 @@
23from canonical.launchpad.webapp.publisher import get_current_browser_request23from canonical.launchpad.webapp.publisher import get_current_browser_request
24from canonical.launchpad.webapp.servers import LaunchpadTestRequest24from canonical.launchpad.webapp.servers import LaunchpadTestRequest
25from canonical.testing.layers import DatabaseFunctionalLayer25from canonical.testing.layers import DatabaseFunctionalLayer
26from lazr.restful import EntryResource
26from lp.testing import (27from lp.testing import (
27 ANONYMOUS,28 ANONYMOUS,
28 login,29 login,
@@ -45,15 +46,17 @@
45 """46 """
46 # This method is completely out of control. Thanks, Zope.47 # This method is completely out of control. Thanks, Zope.
47 name = '+' + self.factory.getUniqueString()48 name = '+' + self.factory.getUniqueString()
49
48 class new_class(simple):50 class new_class(simple):
49 def __init__(self, context, request):51 def __init__(self, context, request):
52 self.context = context
50 view_callable()53 view_callable()
51 required = {}54 required = {}
52 for n in ('browserDefault', '__call__', 'publishTraverse'):55 for n in ('browserDefault', '__call__', 'publishTraverse'):
53 required[n] = CheckerPublic56 required[n] = CheckerPublic
54 defineChecker(new_class, Checker(required))57 defineChecker(new_class, Checker(required))
55 getSiteManager().registerAdapter(58 getSiteManager().registerAdapter(
56 new_class, (ILaunchpadRoot, IDefaultBrowserLayer), Interface, 59 new_class, (ILaunchpadRoot, IDefaultBrowserLayer), Interface,
57 name)60 name)
58 self.addCleanup(61 self.addCleanup(
59 getSiteManager().unregisterAdapter, new_class,62 getSiteManager().unregisterAdapter, new_class,
@@ -74,6 +77,7 @@
74 # traversal in the sense of get_current_browser_request.77 # traversal in the sense of get_current_browser_request.
75 login(ANONYMOUS)78 login(ANONYMOUS)
76 requests = []79 requests = []
80
77 def record_current_request():81 def record_current_request():
78 requests.append(get_current_browser_request())82 requests.append(get_current_browser_request())
79 context, view, request = test_traverse(83 context, view, request = test_traverse(
@@ -96,9 +100,18 @@
96 person = self.factory.makePerson()100 person = self.factory.makePerson()
97 login_person(person)101 login_person(person)
98 users = []102 users = []
103
99 def record_user():104 def record_user():
100 users.append(getUtility(ILaunchBag).user)105 users.append(getUtility(ILaunchBag).user)
101 context, view, request = test_traverse(106 context, view, request = test_traverse(
102 self.registerViewCallable(record_user))107 self.registerViewCallable(record_user))
103 self.assertEqual(1, len(users))108 self.assertEqual(1, len(users))
104 self.assertEqual(person, users[0])109 self.assertEqual(person, users[0])
110
111 def test_webservice_traverse(self):
112 login(ANONYMOUS)
113 product = self.factory.makeProduct()
114 context, view, request = test_traverse(
115 'http://api.launchpad.dev/devel/' + product.name)
116 self.assertEqual(product, context)
117 self.assertIsInstance(view, EntryResource)