Merge lp:~jameinel/bzr/2.2-is-up-to-date into lp:bzr

Proposed by John A Meinel
Status: Merged
Merged at revision: 6033
Proposed branch: lp:~jameinel/bzr/2.2-is-up-to-date
Merge into: lp:bzr
Diff against target: 532 lines (+487/-2) (has conflicts)
3 files modified
bzrlib/plugins/launchpad/__init__.py (+55/-2)
bzrlib/plugins/launchpad/lp_api_lite.py (+170/-0)
bzrlib/plugins/launchpad/test_lp_api_lite.py (+262/-0)
Text conflict in bzrlib/plugins/launchpad/__init__.py
To merge this branch: bzr merge lp:~jameinel/bzr/2.2-is-up-to-date
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+67836@code.launchpad.net

Commit message

Bug #609187: warn the user if a package-import branch doesn't have the most recent source package.

Description of the change

This adds a Branch.open hook for the Launchpad plugin. Thanks to Maxb for the nice reference code for querying Launchpad's getPublishedSources.

I wrote this against bzr/2.2 just to give us the maximum opportunity, in case we want to get it backported into something like Lucid or Natty. I don't know whether we will, so certainly I'm proposing this against 2.5 to start.

Points for discussion:

1) This seems to add very little overhead. I added some timing mutters, and it looks like it adds 100-200ms for the getPublishedSeries call (obviously heavily dependent on latency), and about that much again for the get_tag_dict call.

Unfortunately, I don't think the tags are going to end up cached, because at the point Branch.open is hooked we haven't locked the Branch. But we don't (yet) have a good hook point for "I'm fetching from this branch".

Anyway, the ssh handshake time is multiple seconds, so I felt this was reasonable to be on-by-default. Especially since it should only encounter any overhead when we believe the branch is a package-importer branch.

2) Is my search regex reasonable:
    r'bazaar.launchpad.net.*/(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
    r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'

It actually matches based on the expanded URL (not lp:ubuntu/bzr but bazaar.launchpad.net/+branch/ubuntu/bzr). But I'm just listing the short forms that I've tested:
 lp:ubuntu/bzr
 lp:ubuntu/natty/bzr
 lp:~ubuntu-branches/ubuntu/natty/bzr/natty

The one thing I'm not sure about, is that you might want to not get the message if someone is working on a personal branch. However, maybe they *would* like to be informed that their personal branch doesn't have the latest published source.

3) I think it would make more sense to trigger when you are fetching from a branch. This will also trigger when pushing to a branch (maybe you are pushing up the very revision that is missing). However, that would require defining a new hook point, which seemed a lot more invasive if we ever did want to backport this.

4) Wording and information reporting. We may want a flag so that we only tell the users something if it is out of date. However, I thought in the short term, telling them it was up-to-date is reasonable, because it starts letting them know that we're checking for them.
We could enable this in bzr.dev, however I don't know how many people use bzr.dev + work with packaging branches, such that we'd get real feedback about it.

Martin mentioned that some of the wording could be confusing. I'm certainly not attached to any of it. The current output is:

$ bzr info lp:~ubuntu-branches/ubuntu/natty/bzr/natty
Branch not up-to-date. The most recent published version is 2.3.3-0ubuntu1,
but it is not in the branch tags for:
  bzr+ssh://bazaar.launchpad.net/~ubuntu-branches/ubuntu/natty/bzr/natty/
...

$ bzr info lp:ubuntu/bzr
Found most recent published version: 2.4.0~beta4-4ubuntu2
  in bzr+ssh://bazaar.launchpad.net/%2Bbranch/ubuntu/bzr/
...

$ bzr info lp:ubuntu/natty/bzr # With an intentionally buggy query
Could not find a published version for:
  bzr+ssh://bazaar.launchpad.net/%2Bbranch/ubuntu/bzr/
...

I think Martin had a good point that the first case could be clearer about the package import being out of date.

5) I have pretty good test coverage of the LatestPublication class. I have 3 tests that are slow, which I commented out. One testing a failed connection (by attempting to connect to the loopback). For some reason it takes 1s to timeout here, and I didn't want to inflict that in the general case. 2 tests would connect to Launchpad itself. I don't think we want to run them by default, though having them run somehow would be a good way to alert us if Launchpad changes the api somehow.

6) I don't have test coverage of the branch hook. It isn't clear to me what would make a reliable, useful automated test.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Superseded by: https://code.launchpad.net/~jameinel/bzr/2.5-up-to-date-609187/+merge/67844
Can't actually mark superseded, so I'll set this one back to WIP.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/plugins/launchpad/__init__.py'
2--- bzrlib/plugins/launchpad/__init__.py 2011-04-05 01:12:15 +0000
3+++ bzrlib/plugins/launchpad/__init__.py 2011-07-13 16:24:36 +0000
4@@ -40,19 +40,28 @@
5
6 # see http://wiki.bazaar.canonical.com/Specs/BranchRegistrationTool
7
8+import time
9+
10 # Since we are a built-in plugin we share the bzrlib version
11-from bzrlib import version_info
12
13 from bzrlib.lazy_import import lazy_import
14 lazy_import(globals(), """
15 from bzrlib import (
16+<<<<<<< TREE
17 branch as _mod_branch,
18 ui,
19+=======
20+>>>>>>> MERGE-SOURCE
21 trace,
22 )
23 """)
24
25-from bzrlib import bzrdir
26+from bzrlib import (
27+ branch as _mod_branch,
28+ bzrdir,
29+ lazy_regex,
30+ version_info,
31+ )
32 from bzrlib.commands import (
33 Command,
34 register_command,
35@@ -463,11 +472,55 @@
36 _register_directory()
37
38
39+package_branch = lazy_regex.lazy_compile(
40+ r'bazaar.launchpad.net.*/(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
41+ r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
42+ )
43+def _check_is_up_to_date(the_branch):
44+ m = package_branch.search(the_branch.base)
45+ if m is None:
46+ return
47+ from bzrlib.plugins.launchpad import lp_api_lite
48+ archive, series, project = m.group('archive', 'series', 'project')
49+ if series is not None:
50+ # series is optional, so the regex includes the extra '/', we don't
51+ # want to send that on (it causes Internal Server Errors.)
52+ series = series.strip('/')
53+ t = time.time()
54+ latest_pub = lp_api_lite.LatestPublication(archive, series, project)
55+ latest_ver = latest_pub.get_latest_version()
56+ t_latest_ver = time.time() - t
57+ trace.mutter('LatestPublication.get_latest_version took %.3fs'
58+ % (t_latest_ver,))
59+ if latest_ver is None:
60+ trace.note('Could not find a published version for:\n %s'
61+ % (t, the_branch.base,))
62+ return
63+ t = time.time()
64+ tags = the_branch.tags.get_tag_dict()
65+ t_tag_dict = time.time() - t
66+ trace.mutter('LatestPublication get_tag_dict took: %.3fs', t_tag_dict)
67+ if latest_ver in tags:
68+ trace.note('Found most recent published version: %s\n in %s'
69+ % (latest_ver, the_branch.base))
70+ else:
71+ trace.warning('Branch not up-to-date. The most recent published'
72+ ' version is %s,\nbut it is not in the branch'
73+ ' tags for:\n %s' % (latest_ver, the_branch.base))
74+
75+def _register_hooks():
76+ _mod_branch.Branch.hooks.install_named_hook('open',
77+ _check_is_up_to_date, 'package-branch-up-to-date')
78+
79+
80+_register_hooks()
81+
82 def load_tests(basic_tests, module, loader):
83 testmod_names = [
84 'test_account',
85 'test_register',
86 'test_lp_api',
87+ 'test_lp_api_lite',
88 'test_lp_directory',
89 'test_lp_login',
90 'test_lp_open',
91
92=== added file 'bzrlib/plugins/launchpad/lp_api_lite.py'
93--- bzrlib/plugins/launchpad/lp_api_lite.py 1970-01-01 00:00:00 +0000
94+++ bzrlib/plugins/launchpad/lp_api_lite.py 2011-07-13 16:24:36 +0000
95@@ -0,0 +1,170 @@
96+# Copyright (C) 2011 Canonical Ltd
97+#
98+# This program is free software; you can redistribute it and/or modify
99+# it under the terms of the GNU General Public License as published by
100+# the Free Software Foundation; either version 2 of the License, or
101+# (at your option) any later version.
102+#
103+# This program is distributed in the hope that it will be useful,
104+# but WITHOUT ANY WARRANTY; without even the implied warranty of
105+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
106+# GNU General Public License for more details.
107+#
108+# You should have received a copy of the GNU General Public License
109+# along with this program; if not, write to the Free Software
110+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
111+
112+"""Tools for dealing with the Launchpad API without using launchpadlib.
113+
114+The api itself is a RESTful interface, so we can make HTTP queries directly.
115+loading launchpadlib itself has a fairly high overhead (just calling
116+Launchpad.login_anonymously() takes a 500ms once the WADL is cached, and 5+s to
117+get the WADL.
118+"""
119+
120+try:
121+ # Use simplejson if available, much faster, and can be easily installed in
122+ # older versions of python
123+ import simplejson as json
124+except ImportError:
125+ # Is present since python 2.6
126+ try:
127+ import json
128+ except ImportError:
129+ json = None
130+
131+import urllib
132+import urllib2
133+
134+from bzrlib import trace
135+
136+
137+DEFAULT_SERIES = 'oneiric'
138+
139+class LatestPublication(object):
140+ """Encapsulate how to find the latest publication for a given project."""
141+
142+ LP_API_ROOT = 'https://api.launchpad.net/1.0'
143+
144+ def __init__(self, archive, series, project):
145+ self._archive = archive
146+ self._project = project
147+ self._setup_series_and_pocket(series)
148+
149+ def _setup_series_and_pocket(self, series):
150+ """Parse the 'series' info into a series and a pocket.
151+
152+ eg::
153+ _setup_series_and_pocket('natty-proposed')
154+ => _series == 'natty'
155+ _pocket == 'Proposed'
156+ """
157+ self._series = series
158+ self._pocket = None
159+ if self._series is not None and '-' in self._series:
160+ self._series, self._pocket = self._series.split('-', 1)
161+ self._pocket = self._pocket.title()
162+ else:
163+ self._pocket = 'Release'
164+
165+ def _archive_URL(self):
166+ """Return the Launchpad 'Archive' URL that we will query.
167+ This is everything in the URL except the query parameters.
168+ """
169+ return '%s/%s/+archive/primary' % (self.LP_API_ROOT, self._archive)
170+
171+ def _publication_status(self):
172+ """Handle the 'status' field.
173+ It seems that Launchpad tracks all 'debian' packages as 'Pending', while
174+ for 'ubuntu' we care about the 'Published' packages.
175+ """
176+ if self._archive == 'debian':
177+ # Launchpad only tracks debian packages as "Pending", it doesn't mark
178+ # them Published
179+ return 'Pending'
180+ return 'Published'
181+
182+ def _query_params(self):
183+ """Get the parameters defining our query.
184+ This defines the actions we are making against the archive.
185+ :return: A dict of query parameters.
186+ """
187+ params = {'ws.op': 'getPublishedSources',
188+ 'exact_match': 'true',
189+ # If we need to use "" shouldn't we quote the project somehow?
190+ 'source_name': '"%s"' % (self._project,),
191+ 'status': self._publication_status(),
192+ # We only need the latest one, the results seem to be properly
193+ # most-recent-debian-version sorted
194+ 'ws.size': '1',
195+ }
196+ if self._series is not None:
197+ params['distro_series'] = '/%s/%s' % (self._archive, self._series)
198+ if self._pocket is not None:
199+ params['pocket'] = self._pocket
200+ return params
201+
202+ def _query_URL(self):
203+ """Create the full URL that we need to query, including parameters."""
204+ params = self._query_params()
205+ # We sort to give deterministic results for testing
206+ encoded = urllib.urlencode(sorted(params.items()))
207+ return '%s?%s' % (self._archive_URL(), encoded)
208+
209+ def _get_lp_info(self):
210+ """Place an actual HTTP query against the Launchpad service."""
211+ if json is None:
212+ return None
213+ query_URL = self._query_URL()
214+ try:
215+ req = urllib2.Request(query_URL)
216+ response = urllib2.urlopen(req)
217+ json_info = response.read()
218+ # XXX: We haven't tested the HTTPError
219+ except (urllib2.URLError, urllib2.HTTPError), e:
220+ trace.mutter('failed to place query to %r' % (query_URL,))
221+ trace.log_exception_quietly()
222+ return None
223+ return json_info
224+
225+ def _parse_json_info(self, json_info):
226+ """Parse the json response from Launchpad into objects."""
227+ if json is None:
228+ return None
229+ try:
230+ return json.loads(json_info)
231+ except Exception:
232+ trace.mutter('Failed to parse json info: %r' % (json_info,))
233+ trace.log_exception_quietly()
234+ return None
235+
236+ def get_latest_version(self):
237+ """Get the latest published version for the given package."""
238+ json_info = self._get_lp_info()
239+ if json_info is None:
240+ return None
241+ info = self._parse_json_info(json_info)
242+ if info is None:
243+ return None
244+ try:
245+ entries = info['entries']
246+ if len(entries) == 0:
247+ return None
248+ return entries[0]['source_package_version']
249+ except KeyError:
250+ trace.log_exception_quietly()
251+ return None
252+
253+
254+def get_latest_publication(archive, series, project):
255+ """Get the most recent publication for a given project.
256+
257+ :param archive: Either 'ubuntu' or 'debian'
258+ :param series: Something like 'natty', 'sid', etc. Can be set as None. Can
259+ also include a pocket such as 'natty-proposed'.
260+ :param project: Something like 'bzr'
261+ :return: A version string indicating the most-recent version published in
262+ Launchpad. Might return None if there is an error.
263+ """
264+ lp = LatestPublication(archive, series, project)
265+ return lp.get_latest_version()
266
267=== added file 'bzrlib/plugins/launchpad/test_lp_api_lite.py'
268--- bzrlib/plugins/launchpad/test_lp_api_lite.py 1970-01-01 00:00:00 +0000
269+++ bzrlib/plugins/launchpad/test_lp_api_lite.py 2011-07-13 16:24:36 +0000
270@@ -0,0 +1,262 @@
271+# Copyright (C) 2011 Canonical Ltd
272+#
273+# This program is free software; you can redistribute it and/or modify
274+# it under the terms of the GNU General Public License as published by
275+# the Free Software Foundation; either version 2 of the License, or
276+# (at your option) any later version.
277+#
278+# This program is distributed in the hope that it will be useful,
279+# but WITHOUT ANY WARRANTY; without even the implied warranty of
280+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
281+# GNU General Public License for more details.
282+#
283+# You should have received a copy of the GNU General Public License
284+# along with this program; if not, write to the Free Software
285+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
286+
287+"""Tools for dealing with the Launchpad API without using launchpadlib.
288+"""
289+
290+import socket
291+
292+from bzrlib import tests
293+from bzrlib.plugins.launchpad import lp_api_lite
294+
295+class _JSONParserFeature(tests.Feature):
296+
297+ def _probe(self):
298+ return lp_api_lite.json is not None
299+
300+ def feature_name(self):
301+ return 'simplejson or json'
302+
303+JSONParserFeature = _JSONParserFeature()
304+
305+_example_response = r"""
306+{
307+ "total_size": 2,
308+ "start": 0,
309+ "next_collection_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary?distro_series=%2Fubuntu%2Flucid&exact_match=true&source_name=%22bzr%22&status=Published&ws.op=getPublishedSources&ws.start=1&ws.size=1",
310+ "entries": [
311+ {
312+ "package_creator_link": "https://api.launchpad.net/1.0/~maxb",
313+ "package_signer_link": "https://api.launchpad.net/1.0/~jelmer",
314+ "source_package_name": "bzr",
315+ "removal_comment": null,
316+ "display_name": "bzr 2.1.4-0ubuntu1 in lucid",
317+ "date_made_pending": null,
318+ "source_package_version": "2.1.4-0ubuntu1",
319+ "date_superseded": null,
320+ "http_etag": "\"9ba966152dec474dc0fe1629d0bbce2452efaf3b-5f4c3fbb3eaf26d502db4089777a9b6a0537ffab\"",
321+ "self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/1750327",
322+ "distro_series_link": "https://api.launchpad.net/1.0/ubuntu/lucid",
323+ "component_name": "main",
324+ "status": "Published",
325+ "date_removed": null,
326+ "pocket": "Updates",
327+ "date_published": "2011-05-30T06:09:58.653984+00:00",
328+ "removed_by_link": null,
329+ "section_name": "devel",
330+ "resource_type_link": "https://api.launchpad.net/1.0/#source_package_publishing_history",
331+ "archive_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary",
332+ "package_maintainer_link": "https://api.launchpad.net/1.0/~ubuntu-devel-discuss-lists",
333+ "date_created": "2011-05-30T05:19:12.233621+00:00",
334+ "scheduled_deletion_date": null
335+ }
336+ ]
337+}"""
338+
339+_no_versions_response = '{"total_size": 0, "start": 0, "entries": []}'
340+
341+
342+class TestLatestPublication(tests.TestCase):
343+
344+ def make_latest_publication(self, archive='ubuntu', series='natty',
345+ project='bzr'):
346+ return lp_api_lite.LatestPublication(archive, series, project)
347+
348+ def test_init(self):
349+ latest_pub = self.make_latest_publication()
350+ self.assertEqual('ubuntu', latest_pub._archive)
351+ self.assertEqual('natty', latest_pub._series)
352+ self.assertEqual('bzr', latest_pub._project)
353+ self.assertEqual('Release', latest_pub._pocket)
354+
355+ def test__archive_URL(self):
356+ latest_pub = self.make_latest_publication()
357+ self.assertEqual(
358+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary',
359+ latest_pub._archive_URL())
360+
361+ def test__publication_status_for_ubuntu(self):
362+ latest_pub = self.make_latest_publication()
363+ self.assertEqual('Published', latest_pub._publication_status())
364+
365+ def test__publication_status_for_debian(self):
366+ latest_pub = self.make_latest_publication(archive='debian')
367+ self.assertEqual('Pending', latest_pub._publication_status())
368+
369+ def test_pocket(self):
370+ latest_pub = self.make_latest_publication(series='natty-proposed')
371+ self.assertEqual('natty', latest_pub._series)
372+ self.assertEqual('Proposed', latest_pub._pocket)
373+
374+ def test_series_None(self):
375+ latest_pub = self.make_latest_publication(series=None)
376+ self.assertEqual('ubuntu', latest_pub._archive)
377+ self.assertEqual(None, latest_pub._series)
378+ self.assertEqual('bzr', latest_pub._project)
379+ self.assertEqual('Release', latest_pub._pocket)
380+
381+ def test__query_params(self):
382+ latest_pub = self.make_latest_publication()
383+ self.assertEqual({'ws.op': 'getPublishedSources',
384+ 'exact_match': 'true',
385+ 'source_name': '"bzr"',
386+ 'status': 'Published',
387+ 'ws.size': '1',
388+ 'distro_series': '/ubuntu/natty',
389+ 'pocket': 'Release',
390+ }, latest_pub._query_params())
391+
392+ def test__query_params_no_series(self):
393+ latest_pub = self.make_latest_publication(series=None)
394+ self.assertEqual({'ws.op': 'getPublishedSources',
395+ 'exact_match': 'true',
396+ 'source_name': '"bzr"',
397+ 'status': 'Published',
398+ 'ws.size': '1',
399+ 'pocket': 'Release',
400+ }, latest_pub._query_params())
401+
402+ def test__query_params_pocket(self):
403+ latest_pub = self.make_latest_publication(series='natty-proposed')
404+ self.assertEqual({'ws.op': 'getPublishedSources',
405+ 'exact_match': 'true',
406+ 'source_name': '"bzr"',
407+ 'status': 'Published',
408+ 'ws.size': '1',
409+ 'distro_series': '/ubuntu/natty',
410+ 'pocket': 'Proposed',
411+ }, latest_pub._query_params())
412+
413+ def test__query_URL(self):
414+ latest_pub = self.make_latest_publication()
415+ # we explicitly sort params, so we can be sure this URL matches exactly
416+ self.assertEqual(
417+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary'
418+ '?distro_series=%2Fubuntu%2Fnatty&exact_match=true'
419+ '&pocket=Release&source_name=%22bzr%22&status=Published'
420+ '&ws.op=getPublishedSources&ws.size=1',
421+ latest_pub._query_URL())
422+
423+ def DONT_test__gracefully_handle_failed_rpc_connection(self):
424+ # TODO: This test kind of sucks. We intentionally create an arbitrary
425+ # port and don't listen to it, because we want the request to fail.
426+ # However, it seems to take 1s for it to timeout. Is there a way
427+ # to make it fail faster?
428+ latest_pub = self.make_latest_publication()
429+ s = socket.socket()
430+ s.bind(('127.0.0.1', 0))
431+ addr, port = s.getsockname()
432+ latest_pub.LP_API_ROOT = 'http://%s:%s/' % (addr, port)
433+ s.close()
434+ self.assertIs(None, latest_pub._get_lp_info())
435+
436+ def DONT_test__query_launchpad(self):
437+ # TODO: This is a test that we are making a valid request against
438+ # launchpad. This seems important, but it is slow, requires net
439+ # access, and requires launchpad to be up and running. So for
440+ # now, it is commented out for production tests.
441+ latest_pub = self.make_latest_publication()
442+ json_txt = latest_pub._get_lp_info()
443+ self.assertIsNot(None, json_txt)
444+ if lp_api_lite.json is None:
445+ # We don't have a way to parse the text
446+ return
447+ # The content should be a valid json result
448+ content = lp_api_lite.json.loads(json_txt)
449+ entries = content['entries'] # It should have an 'entries' field.
450+ # ws.size should mean we get 0 or 1, and there should be something
451+ self.assertEqual(1, len(entries))
452+ entry = entries[0]
453+ self.assertEqual('bzr', entry['source_package_name'])
454+ version = entry['source_package_version']
455+ self.assertIsNot(None, version)
456+
457+ def test__get_lp_info_no_json(self):
458+ # If we can't parse the json, we don't make the query.
459+ self.overrideAttr(lp_api_lite, 'json', None)
460+ latest_pub = self.make_latest_publication()
461+ self.assertIs(None, latest_pub._get_lp_info())
462+
463+ def test__parse_json_info_no_module(self):
464+ # If a json parsing module isn't available, we just return None here.
465+ self.overrideAttr(lp_api_lite, 'json', None)
466+ latest_pub = self.make_latest_publication()
467+ self.assertIs(None, latest_pub._parse_json_info(_example_response))
468+
469+ def test__parse_json_example_response(self):
470+ self.requireFeature(JSONParserFeature)
471+ latest_pub = self.make_latest_publication()
472+ content = latest_pub._parse_json_info(_example_response)
473+ self.assertIsNot(None, content)
474+ self.assertEqual(2, content['total_size'])
475+ entries = content['entries']
476+ self.assertEqual(1, len(entries))
477+ entry = entries[0]
478+ self.assertEqual('bzr', entry['source_package_name'])
479+ self.assertEqual("2.1.4-0ubuntu1", entry["source_package_version"])
480+
481+ def test__parse_json_not_json(self):
482+ self.requireFeature(JSONParserFeature)
483+ latest_pub = self.make_latest_publication()
484+ self.assertIs(None, latest_pub._parse_json_info('Not_valid_json'))
485+
486+ def test_get_latest_version_no_response(self):
487+ latest_pub = self.make_latest_publication()
488+ latest_pub._get_lp_info = lambda: None
489+ self.assertEqual(None, latest_pub.get_latest_version())
490+
491+ def test_get_latest_version_no_json(self):
492+ self.overrideAttr(lp_api_lite, 'json', None)
493+ latest_pub = self.make_latest_publication()
494+ self.assertEqual(None, latest_pub.get_latest_version())
495+
496+ def test_get_latest_version_invalid_json(self):
497+ self.requireFeature(JSONParserFeature)
498+ latest_pub = self.make_latest_publication()
499+ latest_pub._get_lp_info = lambda: "not json"
500+ self.assertEqual(None, latest_pub.get_latest_version())
501+
502+ def test_get_latest_version_no_versions(self):
503+ self.requireFeature(JSONParserFeature)
504+ latest_pub = self.make_latest_publication()
505+ latest_pub._get_lp_info = lambda: _no_versions_response
506+ self.assertEqual(None, latest_pub.get_latest_version())
507+
508+ def test_get_latest_version_missing_entries(self):
509+ # Launchpad's no-entries response does have an empty entries value.
510+ # However, lets test that we handle other failures without tracebacks
511+ self.requireFeature(JSONParserFeature)
512+ latest_pub = self.make_latest_publication()
513+ latest_pub._get_lp_info = lambda: '{}'
514+ self.assertEqual(None, latest_pub.get_latest_version())
515+
516+ def test_get_latest_version_invalid_entries(self):
517+ # Make sure we sanely handle a json response we don't understand
518+ self.requireFeature(JSONParserFeature)
519+ latest_pub = self.make_latest_publication()
520+ latest_pub._get_lp_info = lambda: '{"entries": {"a": 1}}'
521+ self.assertEqual(None, latest_pub.get_latest_version())
522+
523+ def test_get_latest_version_example(self):
524+ self.requireFeature(JSONParserFeature)
525+ latest_pub = self.make_latest_publication()
526+ latest_pub._get_lp_info = lambda: _example_response
527+ self.assertEqual("2.1.4-0ubuntu1", latest_pub.get_latest_version())
528+
529+ def DONT_test_get_latest_version_from_launchpad(self):
530+ self.requireFeature(JSONParserFeature)
531+ latest_pub = self.make_latest_publication()
532+ self.assertIsNot(None, latest_pub.get_latest_version())