Merge lp:~sinzui/launchpad/librarian-merge-proposal into lp:launchpad

Proposed by Curtis Hovey on 2012-11-29
Status: Merged
Approved by: j.c.sackett on 2012-11-29
Approved revision: no longer in the source branch.
Merged at revision: 16330
Proposed branch: lp:~sinzui/launchpad/librarian-merge-proposal
Merge into: lp:launchpad
Diff against target: 546 lines (+163/-75)
10 files modified
lib/lp/code/browser/branchmergeproposal.py (+13/-0)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+23/-0)
lib/lp/code/model/branchmergeproposal.py (+1/-1)
lib/lp/code/model/diff.py (+25/-1)
lib/lp/code/model/tests/test_diff.py (+28/-0)
lib/lp/code/templates/branchmergeproposal-diff.pt (+4/-0)
lib/lp/scripts/utilities/importfascist.py (+44/-67)
lib/lp/services/librarian/interfaces/client.py (+4/-1)
lib/lp/services/librarian/tests/test_client.py (+20/-5)
lib/lp/services/webapp/adapter.py (+1/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/librarian-merge-proposal
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-11-29 Approve on 2012-11-29
Review via email: mp+137039@code.launchpad.net

Commit Message

Allow merge proposals to operate without the launchpad librarian.

Description of the Change

When the librarian is not available, merge proposal pages will not
load at all. We expect the librarian to not be available for as much
as an hour when it is upgraded to precises, but we want Lp to be
operational.

--------------------------------------------------------------------

RULES

    Pre-implementation: wgrant
    * Diff.text opens the LFA, self.diff_text.open(), without specifying
      the timeout.
      * the default timeout is 5 seconds, which is the entire time Lp
        allocates to a request. The 5s timeout was added when Lp had a
        30s request time in 2009. It is impossible for the timeout to be
        raised from with the webapp. Maybe the default should be 4 seconds.
        when the the webapp is making the request.
        LIBRARIAN_SERVER_DEFAULT_TIMEOUT = 5
      * Diff.text can pass a timout that is 1s or 5s minus the remaing
        time to ensure that the request time does not run out. After
        some discussion, we believe 2s is more than amp for the
        the librarian...anything requiring more than cannot be shown
        in the page.
      * The LibrarianServerError needs to be wrapped for the web service.
    * DiffRenderingMixin.preview_diff_text wraps the call to Diff.text to
      sanitise and recover from exceptions.
      * The property could catch the LibrarianServerError raise when the
        open attempts exceed the timeout.
      * The diff_oversized property just counts lines to return a bool.
        It could return True if preview_diff_text handled the timeout.

    ADDENDUM:
    * I had a fight with with the import fascist. I won, by removing
      all th ancient cruft, but had to give some ground by acknowledging
      that there are dubious imports in some browser modules.
    * There was more lint then expected in the old modules.

QA

    * Visit https://code.qastaging.launchpad.net/~sinzui/impressive/lirc-0/+merge/29161
    * Verify the page loads and that it states that the diff could not
      be retrieved at this time.

LINT

    lib/lp/code/browser/branchmergeproposal.py
    lib/lp/code/browser/tests/test_branchmergeproposal.py
    lib/lp/code/model/diff.py
    lib/lp/code/model/tests/test_diff.py
    lib/lp/code/templates/branchmergeproposal-diff.pt
    lib/lp/scripts/utilities/importfascist.py
    lib/lp/services/librarian/interfaces/client.py
    lib/lp/services/librarian/tests/test_client.py
    lib/lp/services/webapp/adapter.py

LoC

    I have more than 16,000 line credit.

TEST

    ./bin/test -vvc lp.code.browser.tests.test_branchmergeproposal
    ./bin/test -vvc lp.code.model.tests.test_diff
    ./bin/test -vvc lp.services.librarian.tests.test_client

IMPLEMENTATION

I updated view to try and recover from a failure to get the diff from
the librarian. The page will explain that the diff wasn't available, but
the user can reload or download it.
    lib/lp/code/browser/branchmergeproposal.py
    lib/lp/code/browser/tests/test_branchmergeproposal.py
    lib/lp/code/templates/branchmergeproposal-diff.pt

I changed the model to pass the timeout time to the LibrarianFileAlias. The
rules are a but complicated. Scripts run without timeouts. The webapp
should get the diff within 2 seconds -- otherwise something is very wrong.
If there is less that 2 seconds left in the request, the code shaves a bit
of time to set the timeout.
    lib/lp/services/webapp/adapter.py
    lib/lp/code/model/diff.py
    lib/lp/code/model/tests/test_diff.py

I exported the LibrarianServerError as a httplib.TIMEOUT error.
    lib/lp/services/librarian/interfaces/client.py
    lib/lp/services/librarian/tests/test_client.py

I removed all the checks for non-existent paths. The changes illustrate
how much Lp has changes since 2009. I re-enabled the database checks,
but the rules are clearly difficult to enforce. I added a list of
dubious imports so that the code continues to build even though
we think the design is wrong.
    lib/lp/scripts/utilities/importfascist.py

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

With the fix of the typo and comments we discussed in IRC, this looks good.

Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
2--- lib/lp/code/browser/branchmergeproposal.py 2012-11-26 08:40:20 +0000
3+++ lib/lp/code/browser/branchmergeproposal.py 2012-11-30 19:11:22 +0000
4@@ -107,6 +107,7 @@
5 Summary,
6 Whiteboard,
7 )
8+from lp.services.librarian.interfaces.client import LibrarianServerError
9 from lp.services.messages.interfaces.message import IMessageSet
10 from lp.services.propertycache import cachedproperty
11 from lp.services.webapp import (
12@@ -517,9 +518,18 @@
13 class DiffRenderingMixin:
14 """A mixin class for handling diff text."""
15
16+ @property
17+ def diff_available(self):
18+ """Is the preview diff available from the librarian?"""
19+ if getattr(self, '_diff_available', None) is None:
20+ # Load the cache so that the answer is known.
21+ self.preview_diff_text
22+ return self._diff_available
23+
24 @cachedproperty
25 def preview_diff_text(self):
26 """Return a (hopefully) intelligently encoded review diff."""
27+ self._diff_available = True
28 preview_diff = self.preview_diff
29 if preview_diff is None:
30 return None
31@@ -527,6 +537,9 @@
32 diff = preview_diff.text.decode('utf-8')
33 except UnicodeDecodeError:
34 diff = preview_diff.text.decode('windows-1252', 'replace')
35+ except LibrarianServerError:
36+ self._diff_available = False
37+ diff = ''
38 # Strip off the trailing carriage returns.
39 return diff.rstrip('\n')
40
41
42=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
43--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2012-10-08 07:15:07 +0000
44+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2012-11-30 19:11:22 +0000
45@@ -57,6 +57,7 @@
46 PersonVisibility,
47 TeamMembershipPolicy,
48 )
49+from lp.services.librarian.interfaces.client import LibrarianServerError
50 from lp.services.messages.model.message import MessageSet
51 from lp.services.webapp import canonical_url
52 from lp.services.webapp.interfaces import (
53@@ -69,6 +70,7 @@
54 BrowserTestCase,
55 feature_flags,
56 login_person,
57+ monkey_patch,
58 person_logged_in,
59 set_feature_flag,
60 TestCaseWithFactory,
61@@ -865,6 +867,7 @@
62 view = create_initialized_view(self.bmp, '+index')
63 self.assertEqual(diff_bytes.decode('utf-8'),
64 view.preview_diff_text)
65+ self.assertTrue(view.diff_available)
66
67 def test_preview_diff_all_chars(self):
68 """preview_diff should work on diffs containing all possible bytes."""
69@@ -875,6 +878,26 @@
70 view = create_initialized_view(self.bmp, '+index')
71 self.assertEqual(diff_bytes.decode('windows-1252', 'replace'),
72 view.preview_diff_text)
73+ self.assertTrue(view.diff_available)
74+
75+ def test_preview_diff_timeout(self):
76+ # The preview_diff will recover from a timeout set to get the
77+ # librarian content.
78+ text = ''.join(chr(x) for x in range(255))
79+ diff_bytes = ''.join(unified_diff('', text))
80+ self.setPreviewDiff(diff_bytes)
81+ transaction.commit()
82+
83+ def fake_open(*args):
84+ raise LibrarianServerError
85+
86+ lfa = removeSecurityProxy(self.bmp.preview_diff).diff.diff_text
87+ with monkey_patch(lfa, open=fake_open):
88+ view = create_initialized_view(self.bmp.preview_diff, '+diff')
89+ self.assertEqual('', view.preview_diff_text)
90+ self.assertFalse(view.diff_available)
91+ markup = view()
92+ self.assertIn('The diff is not available at this time.', markup)
93
94 def setPreviewDiff(self, preview_diff_bytes):
95 preview_diff = PreviewDiff.create(
96
97=== modified file 'lib/lp/code/model/branchmergeproposal.py'
98--- lib/lp/code/model/branchmergeproposal.py 2012-09-19 01:19:35 +0000
99+++ lib/lp/code/model/branchmergeproposal.py 2012-11-30 19:11:22 +0000
100@@ -415,7 +415,7 @@
101 elif status == BranchMergeProposalStatus.MERGE_FAILED:
102 self._transitionToState(status, user=user)
103 else:
104- raise AssertionError('Unexpected queue status: ' % status)
105+ raise AssertionError('Unexpected queue status: %s' % status)
106
107 def setAsWorkInProgress(self):
108 """See `IBranchMergeProposal`."""
109
110=== modified file 'lib/lp/code/model/diff.py'
111--- lib/lp/code/model/diff.py 2012-10-02 01:25:48 +0000
112+++ lib/lp/code/model/diff.py 2012-11-30 19:11:22 +0000
113@@ -51,10 +51,14 @@
114 from lp.services.database.bulk import load_referencing
115 from lp.services.database.sqlbase import SQLBase
116 from lp.services.librarian.interfaces import ILibraryFileAliasSet
117+from lp.services.librarian.interfaces.client import (
118+ LIBRARIAN_SERVER_DEFAULT_TIMEOUT,
119+ )
120 from lp.services.propertycache import (
121 cachedproperty,
122 get_property_cache,
123 )
124+from lp.services.webapp.adapter import get_request_remaining_seconds
125
126
127 class Diff(SQLBase):
128@@ -94,12 +98,32 @@
129 if self.diff_text is None:
130 return ''
131 else:
132- self.diff_text.open()
133+ self.diff_text.open(self._getDiffTimeout())
134 try:
135 return self.diff_text.read(config.diff.max_read_size)
136 finally:
137 self.diff_text.close()
138
139+ def _getDiffTimeout(self):
140+ """Return the seconds allocated to get the diff from the librarian.
141+
142+ the value will be Non for scripts, 2 for the webapp, or if thre is
143+ little request time left, the number will be smaller or equal to
144+ the remaining request time.
145+ """
146+ remaining = get_request_remaining_seconds()
147+ if remaining is None:
148+ return LIBRARIAN_SERVER_DEFAULT_TIMEOUT
149+ elif remaining > 2.0:
150+ # The maximum permitted time for webapp requests.
151+ return 2.0
152+ elif remaining > 0.01:
153+ # Shave off 1 hundreth of a second off so that the call site
154+ # has a chance to recover.
155+ return remaining - 0.01
156+ else:
157+ return remaining
158+
159 @property
160 def oversized(self):
161 # If the size of the content of the librarian file is over the
162
163=== modified file 'lib/lp/code/model/tests/test_diff.py'
164--- lib/lp/code/model/tests/test_diff.py 2012-01-01 02:58:52 +0000
165+++ lib/lp/code/model/tests/test_diff.py 2012-11-30 19:11:22 +0000
166@@ -29,11 +29,15 @@
167 PreviewDiff,
168 )
169 from lp.code.model.directbranchcommit import DirectBranchCommit
170+from lp.services.librarian.interfaces.client import (
171+ LIBRARIAN_SERVER_DEFAULT_TIMEOUT,
172+ )
173 from lp.services.webapp import canonical_url
174 from lp.services.webapp.testing import verifyObject
175 from lp.testing import (
176 login,
177 login_person,
178+ monkey_patch,
179 TestCaseWithFactory,
180 )
181 from lp.testing.layers import (
182@@ -172,6 +176,30 @@
183 diff = self._create_diff(content)
184 self.assertTrue(diff.oversized)
185
186+ def test_getDiffTimeout(self):
187+ # The time permitted to get the diff from the librarian may be None,
188+ # or 2. If there is not 2 seconds left in the request, the number will
189+ # will 0.01 smaller or the actual remaining time.
190+ content = ''.join(unified_diff('', "1234567890" * 10))
191+ diff = self._create_diff(content)
192+ value = None
193+
194+ def fake():
195+ return value
196+
197+ from lp.code.model import diff as diff_module
198+ with monkey_patch(diff_module, get_request_remaining_seconds=fake):
199+ self.assertIs(
200+ LIBRARIAN_SERVER_DEFAULT_TIMEOUT, diff._getDiffTimeout())
201+ value = 3.1
202+ self.assertEqual(2.0, diff._getDiffTimeout())
203+ value = 1.11
204+ self.assertEqual(1.1, diff._getDiffTimeout())
205+ value = 0.11
206+ self.assertEqual(0.1, diff._getDiffTimeout())
207+ value = 0.01
208+ self.assertEqual(0.01, diff._getDiffTimeout())
209+
210
211 class TestDiffInScripts(DiffTestCase):
212
213
214=== modified file 'lib/lp/code/templates/branchmergeproposal-diff.pt'
215--- lib/lp/code/templates/branchmergeproposal-diff.pt 2010-02-17 19:10:51 +0000
216+++ lib/lp/code/templates/branchmergeproposal-diff.pt 2012-11-30 19:11:22 +0000
217@@ -17,6 +17,10 @@
218 </div>
219 <div class="boardCommentBody attachmentBody">
220 <tal:diff replace="structure view/preview_diff_text/fmt:diff" />
221+ <tal:diff-not-available condition="not: view/diff_available">
222+ The diff is not available at this time. You can reload the
223+ page or download it.
224+ </tal:diff-not-available>
225 </div>
226 <div class="boardCommentFooter"
227 tal:condition="view/diff_oversized">
228
229=== modified file 'lib/lp/scripts/utilities/importfascist.py'
230--- lib/lp/scripts/utilities/importfascist.py 2012-01-19 17:23:59 +0000
231+++ lib/lp/scripts/utilities/importfascist.py 2012-11-30 19:11:22 +0000
232@@ -9,7 +9,6 @@
233
234
235 original_import = __builtin__.__import__
236-database_root = 'canonical.launchpad.database'
237 naughty_imports = set()
238
239 # Silence bogus warnings from Hardy's python-pkg-resources package.
240@@ -22,39 +21,6 @@
241 return set(line.strip() for line in text.splitlines() if line.strip())
242
243
244-permitted_database_imports = text_lines_to_set("""
245- canonical.archivepublisher.deathrow
246- canonical.archivepublisher.domination
247- canonical.archivepublisher.ftparchive
248- canonical.archivepublisher.publishing
249- lp.codehosting.inmemory
250- canonical.launchpad.browser.branchlisting
251- lp.code.browser.branchlisting
252- lp.services.librarian.browser
253- canonical.launchpad.feed.branch
254- lp.code.feed.branch
255- lp.scripts.garbo
256- lp.bugs.vocabularies
257- lp.registry.interfaces.person
258- lp.registry.vocabularies
259- lp.services.worlddata.vocabularies
260- lp.soyuz.vocabularies
261- lp.translations.vocabularies
262- lp.services.librarian.client
263- lp.services.librarianserver.db
264- doctest
265- """)
266-
267-
268-warned_database_imports = text_lines_to_set("""
269- lp.soyuz.scripts.obsolete_distroseries
270- lp.soyuz.scripts.gina.handlers
271- lp.registry.browser.distroseries
272- lp.translations.scripts.po_import
273- lp.systemhomes
274- """)
275-
276-
277 # Sometimes, third-party modules don't export all of their public APIs through
278 # __all__. The following dict maps from such modules to a list of attributes
279 # that are allowed to be imported, whether or not they are in __all__.
280@@ -76,27 +42,51 @@
281 }
282
283
284+unsafe_parts = set(['browser', 'feed', 'xmlrpc', 'widgets'])
285+
286+dubious = [
287+ 'lp.answers.browser.question',
288+ 'lp.app.browser.vocabulary',
289+ 'lp.blueprints.browser.sprint',
290+ 'lp.bugs.browser.bug',
291+ 'lp.bugs.browser.bugalsoaffects',
292+ 'lp.bugs.browser.bugsubscription',
293+ 'lp.bugs.browser.bugtarget',
294+ 'lp.bugs.browser.bugtask',
295+ 'lp.bugs.browser.person',
296+ 'lp.code.browser.branchlisting',
297+ 'lp.code.browser.sourcepackagerecipe',
298+ 'lp.registry.browser.distroseries',
299+ 'lp.registry.browser.distroseriesdifference',
300+ 'lp.registry.browser.milestone',
301+ 'lp.registry.browser.pillar',
302+ 'lp.registry.browser.person',
303+ 'lp.registry.browser.project',
304+ 'lp.registry.browser.sourcepackage',
305+ 'lp.soyuz.browser.archive',
306+ 'lp.soyuz.browser.builder',
307+ 'lp.soyuz.browser.queue',
308+ 'lp.translations.browser.potemplate',
309+ 'lp.translations.browser.serieslanguage',
310+ 'lp.translations.browser.sourcepackage',
311+ 'lp.translations.browser.translationlinksaggregator',
312+ 'lp.translations.browser.translationtemplatesbuild',
313+ ]
314+
315+
316 def database_import_allowed_into(module_path):
317- """Return True if database code is allowed to be imported into the given
318- module path. Otherwise, returns False.
319+ """Return True if model code can be imported into the module path.
320
321 It is allowed if:
322 - The import was made with the __import__ hook.
323- - The importer is from within canonical.launchpad.database.
324 - The importer is a 'test' module.
325- - The importer is in the set of permitted_database_imports.
326- - The importer is within a model module or package.
327-
328- Note that being in the set of warned_database_imports does not make
329- the import allowed.
330-
331+ - The importer is in a nodule that does not face users.
332+ - The import is recognised to be dubious, but not a priority to fix.
333 """
334- if (module_path == '__import__ hook' or
335- module_path.startswith('canonical.launchpad.database') or
336- '.model' in module_path or
337- is_test_module(module_path)):
338- return True
339- return module_path in permitted_database_imports
340+ module_parts = set(module_path.split('.'))
341+ return (unsafe_parts.isdisjoint(module_parts)
342+ or is_test_module(module_path)
343+ or module_path in dubious)
344
345
346 def is_test_module(module_path):
347@@ -105,9 +95,7 @@
348 Otherwise returns False.
349 """
350 name_splitted = module_path.split('.')
351- return ('tests' in name_splitted or
352- 'ftests' in name_splitted or
353- 'testing' in name_splitted)
354+ return ('tests' in name_splitted or 'testing' in name_splitted)
355
356
357 class attrsgetter:
358@@ -166,9 +154,7 @@
359
360
361 class NotFoundPolicyViolation(JackbootError):
362- """import of zope.exceptions.NotFoundError into
363- canonical.launchpad.database.
364- """
365+ """import of zope.exceptions.NotFoundError into lp models modules."""
366
367 def __init__(self, import_into):
368 JackbootError.__init__(self, import_into, '')
369@@ -182,7 +168,6 @@
370 # The names of the arguments form part of the interface of __import__(...),
371 # and must not be changed, as code may choose to invoke __import__ using
372 # keyword arguments - e.g. the encodings module in Python 2.6.
373-# pylint: disable-msg=W0102,W0602
374 def import_fascist(name, globals={}, locals={}, fromlist=[], level=-1):
375 global naughty_imports
376
377@@ -223,25 +208,17 @@
378 import_into = '__import__ hook'
379
380 # Check the "NotFoundError" policy.
381- if (import_into.startswith('canonical.launchpad.database') and
382- name == 'zope.exceptions'):
383+ if ('.model.' in import_into and name == 'zope.exceptions'):
384 if fromlist and 'NotFoundError' in fromlist:
385 raise NotFoundPolicyViolation(import_into)
386
387 # Check the database import policy.
388- if (name.startswith(database_root) and
389- not database_import_allowed_into(import_into)):
390+ if '.model.' in name and not database_import_allowed_into(import_into):
391 error = DatabaseImportPolicyViolation(import_into, name)
392 naughty_imports.add(error)
393- # Raise an error except in the case of browser.traversers.
394- # This exception to raising an error is only temporary, until
395- # browser.traversers is cleaned up.
396- if import_into not in warned_database_imports:
397- raise error
398
399 # Check the import from __all__ policy.
400- if fromlist is not None and (
401- import_into.startswith('canonical') or import_into.startswith('lp')):
402+ if fromlist is not None and import_into.startswith('lp'):
403 # We only want to warn about "from foo import bar" violations in our
404 # own code.
405 fromlist = list(fromlist)
406
407=== modified file 'lib/lp/services/librarian/interfaces/client.py'
408--- lib/lp/services/librarian/interfaces/client.py 2011-12-30 01:14:33 +0000
409+++ lib/lp/services/librarian/interfaces/client.py 2012-11-30 19:11:22 +0000
410@@ -14,8 +14,10 @@
411 'UploadFailed',
412 ]
413
414+import httplib
415 import signal
416
417+from lazr.restful.declarations import error_status
418 from zope.interface import Interface
419
420
421@@ -35,6 +37,7 @@
422 pass
423
424
425+@error_status(httplib.REQUEST_TIMEOUT)
426 class LibrarianServerError(Exception):
427 """An error indicating that the Librarian server is not responding."""
428
429@@ -91,7 +94,7 @@
430
431 def getURLForAlias(aliasID, secure=False):
432 """Returns the URL to the given file.
433-
434+
435 :param aliasID: The LibraryFileAlias for the file to get. A DB lookup
436 will be done for this - if many are to be calculated, eagar loading
437 is recommended.
438
439=== modified file 'lib/lp/services/librarian/tests/test_client.py'
440--- lib/lp/services/librarian/tests/test_client.py 2012-09-28 06:01:02 +0000
441+++ lib/lp/services/librarian/tests/test_client.py 2012-11-30 19:11:22 +0000
442@@ -2,6 +2,7 @@
443 # GNU Affero General Public License version 3 (see the file LICENSE).
444
445 from cStringIO import StringIO
446+import httplib
447 import textwrap
448 import unittest
449 from urllib2 import (
450@@ -25,8 +26,10 @@
451 from lp.services.librarian.model import LibraryFileAlias
452 from lp.testing.layers import (
453 DatabaseLayer,
454+ FunctionalLayer,
455 LaunchpadFunctionalLayer,
456 )
457+from lp.testing.views import create_webservice_error_view
458
459
460 class InstrumentedLibrarianClient(LibrarianClient):
461@@ -36,12 +39,14 @@
462 self.check_error_calls = 0
463
464 sentDatabaseName = False
465+
466 def _sendHeader(self, name, value):
467 if name == 'Database-Name':
468 self.sentDatabaseName = True
469 return LibrarianClient._sendHeader(self, name, value)
470
471 called_getURLForDownload = False
472+
473 def _getURLForDownload(self, aliasID):
474 self.called_getURLForDownload = True
475 return LibrarianClient._getURLForDownload(self, aliasID)
476@@ -79,7 +84,7 @@
477 def test_addFileSendsDatabaseName(self):
478 # addFile should send the Database-Name header.
479 client = InstrumentedLibrarianClient()
480- id1 = client.addFile(
481+ client.addFile(
482 'sample.txt', 6, StringIO('sample'), 'text/plain')
483 self.failUnless(client.sentDatabaseName,
484 "Database-Name header not sent by addFile")
485@@ -91,7 +96,7 @@
486 # different process, we need to explicitly tell the DatabaseLayer to
487 # fully tear down and set up the database.
488 DatabaseLayer.force_dirty_database()
489- id1 = client.remoteAddFile('sample.txt', 6, StringIO('sample'),
490+ client.remoteAddFile('sample.txt', 6, StringIO('sample'),
491 'text/plain')
492 self.failUnless(client.sentDatabaseName,
493 "Database-Name header not sent by remoteAddFile")
494@@ -180,7 +185,7 @@
495 # If the alias has been deleted, _getURLForDownload returns None.
496 lfa = LibraryFileAlias.get(alias_id)
497 lfa.content = None
498- call = block_implicit_flushes( # Prevent a ProgrammingError
499+ call = block_implicit_flushes( # Prevent a ProgrammingError
500 LibrarianClient._getURLForDownload)
501 self.assertEqual(call(client, alias_id), None)
502 finally:
503@@ -216,7 +221,7 @@
504 # If the alias has been deleted, _getURLForDownload returns None.
505 lfa = LibraryFileAlias.get(alias_id)
506 lfa.content = None
507- call = block_implicit_flushes( # Prevent a ProgrammingError
508+ call = block_implicit_flushes( # Prevent a ProgrammingError
509 RestrictedLibrarianClient._getURLForDownload)
510 self.assertEqual(call(client, alias_id), None)
511 finally:
512@@ -231,7 +236,7 @@
513 client = InstrumentedLibrarianClient()
514 alias_id = client.addFile(
515 'sample.txt', 6, StringIO('sample'), 'text/plain')
516- transaction.commit() # Make sure the file is in the "remote" database
517+ transaction.commit() # Make sure the file is in the "remote" database.
518 self.failIf(client.called_getURLForDownload)
519 # (Test:)
520 f = client.getFileByAlias(alias_id)
521@@ -306,3 +311,13 @@
522 client.getFileByAlias(alias_id), 'This is a fake file object', 3)
523
524 client_module._File = _File
525+
526+
527+class TestWebServiceErrors(unittest.TestCase):
528+ """ Test that errors are correctly mapped to HTTP status codes."""
529+
530+ layer = FunctionalLayer
531+
532+ def test_LibrarianServerError_timeout(self):
533+ error_view = create_webservice_error_view(LibrarianServerError())
534+ self.assertEqual(httplib.REQUEST_TIMEOUT, error_view.status)
535
536=== modified file 'lib/lp/services/webapp/adapter.py'
537--- lib/lp/services/webapp/adapter.py 2012-11-14 09:27:39 +0000
538+++ lib/lp/services/webapp/adapter.py 2012-11-30 19:11:22 +0000
539@@ -89,6 +89,7 @@
540 'RequestExpired',
541 'set_request_started',
542 'clear_request_started',
543+ 'get_request_remaining_seconds',
544 'get_request_statements',
545 'get_request_start_time',
546 'get_request_duration',