Merge lp:~jameinel/bzr/2.1-launchpad-package-freshness-609187 into lp:bzr/2.1

Proposed by John A Meinel
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 4881
Proposed branch: lp:~jameinel/bzr/2.1-launchpad-package-freshness-609187
Merge into: lp:bzr/2.1
Diff against target: 987 lines (+931/-4)
4 files modified
NEWS (+25/-0)
bzrlib/plugins/launchpad/__init__.py (+64/-4)
bzrlib/plugins/launchpad/lp_api_lite.py (+285/-0)
bzrlib/plugins/launchpad/test_lp_api_lite.py (+557/-0)
To merge this branch: bzr merge lp:~jameinel/bzr/2.1-launchpad-package-freshness-609187
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Review via email: mp+71542@code.launchpad.net

Commit message

Bug #609187, whenever accessing a Launchpad packaging branch, check that it has the most recent package version.

Description of the change

This is a backport of my Launchpad plugin changes for bug #609187. This makes bzr-2.1 check for the most recent package versions available from Launchpad's database whenever we access a debian or ubuntu branch on Launchpad.

This should cause conflicts when merging up to bzr-2.4 because there were some bzrlib incompatibilities. Namely:

1) TestCase.overrideAttr doesn't exist in bzr-2.1, so I had to go back to manually overriding attributes and cleaning them up.

2) graph.iter_lefthand_ancestry was introduced in 2.4 (IIRC) to replace repository.iter_reverse_revision_history. So I had to write the 2.1 code to use iter_reverse_revision_history.

When we land this, I'll take care to merge up in the most compatible fashion I can. (So as soon as overrideAttr exists in a bzr release, I'll switch back to using it, similarly for iter_reverse_revision_history changes.)

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

I have only briefly glanced over this. I've specifically checked the overriding of attributes and iter_reverse_revision_history. That all looks good to me.

It would be useful to test this with 2.4, in case this uses anything that relies on 2.5 or 2.6. I can't find anything when looking manually.

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 8/16/2011 4:27 PM, Jelmer Vernooij wrote:
> Review: Approve I have only briefly glanced over this. I've
> specifically checked the overriding of attributes and
> iter_reverse_revision_history. That all looks good to me.
>
> It would be useful to test this with 2.4, in case this uses anything
> that relies on 2.5 or 2.6. I can't find anything when looking
> manually.

I'm not sure what you mean by 2.4. But we already have this landed into
the 2.4 series, using the newer apis.

When I merge this up, I plan on using whatever apis are available at
each stage.

And I did make sure that:

a) The test suite passes
b) Activating the "DONT_*" tests that actually connect to launchpad and
make a query and parse the result also still pass.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk5KfxoACgkQJdeBCYSNAAMr3QCgt8cHcfB33bSHg+tPnNzFKY5I
/p8AnRZpY9lxZZyQ9uNUg/mNCcgSPzqj
=RlmU
-----END PGP SIGNATURE-----

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

On 16/08/11 16:30, John Arbash Meinel wrote:
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> On 8/16/2011 4:27 PM, Jelmer Vernooij wrote:
>> Review: Approve I have only briefly glanced over this. I've
>> specifically checked the overriding of attributes and
>> iter_reverse_revision_history. That all looks good to me.
>>
>> It would be useful to test this with 2.4, in case this uses anything
>> that relies on 2.5 or 2.6. I can't find anything when looking
>> manually.
> I'm not sure what you mean by 2.4. But we already have this landed into
> the 2.4 series, using the newer apis.
Sorry, that was ambiguous. I meant compatible with python 2.4,
considering we still try to support it for bzr 2.1. I'm also not sure
what PQM uses to land things on lp:bzr/2.1 these days (if it's python2.6
we need to be careful).

Cheers,

Jelmer

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 8/16/2011 4:42 PM, Jelmer Vernooij wrote:
> On 16/08/11 16:30, John Arbash Meinel wrote:
>> -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA1
>>
>> On 8/16/2011 4:27 PM, Jelmer Vernooij wrote:
>>> Review: Approve I have only briefly glanced over this. I've
>>> specifically checked the overriding of attributes and
>>> iter_reverse_revision_history. That all looks good to me.
>>>
>>> It would be useful to test this with 2.4, in case this uses
>>> anything that relies on 2.5 or 2.6. I can't find anything when
>>> looking manually.
>> I'm not sure what you mean by 2.4. But we already have this landed
>> into the 2.4 series, using the newer apis.
> Sorry, that was ambiguous. I meant compatible with python 2.4,
> considering we still try to support it for bzr 2.1. I'm also not
> sure what PQM uses to land things on lp:bzr/2.1 these days (if it's
> python2.6 we need to be careful).
>
> Cheers,
>
> Jelmer

Ah, good point. No we don't test with python 2.4 anywhere, and I don't
have it installed on my system anymore.

lp_api_lite is fairly large, but I don't think I have any 2.4-isms in
there. Your point is well made, though, I probably should try to dig
python-2.4 out.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk5KggAACgkQJdeBCYSNAANkkwCeI8IXUzFM60/MppodvoPBsyqn
LG4AnicZX0dS3komAZxTfehQzA/VtQSX
=EtOu
-----END PGP SIGNATURE-----

Revision history for this message
John A Meinel (jameinel) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2011-05-17 08:19:08 +0000
3+++ NEWS 2011-08-15 10:49:24 +0000
4@@ -19,6 +19,31 @@
5 Bug Fixes
6 *********
7
8+* Accessing a packaging branch on Launchpad (eg, ``lp:ubuntu/bzr``) now
9+ checks to see if the most recent published source package version for
10+ that project is present in the branch tags. This should help developers
11+ trust whether the packaging branch is up-to-date and can be used for new
12+ changes. The level of verbosity is controlled by the config item
13+ ``launchpad.packaging_verbosity``. It can be set to one of
14+
15+ off
16+ disable all checks
17+
18+
19+ minimal
20+ only display if the branch is out-of-date
21+
22+ short
23+ also display single-line up-to-date and missing,
24+
25+
26+ all
27+ (default) display multi-line content for all states
28+
29+
30+ (John Arbash Meinel, #609187, #812928)
31+
32+
33 Improvements
34 ************
35
36
37=== modified file 'bzrlib/plugins/launchpad/__init__.py'
38--- bzrlib/plugins/launchpad/__init__.py 2010-11-30 20:42:42 +0000
39+++ bzrlib/plugins/launchpad/__init__.py 2011-08-15 10:49:24 +0000
40@@ -21,18 +21,20 @@
41
42 # see http://bazaar-vcs.org/Specs/BranchRegistrationTool
43
44-# Since we are a built-in plugin we share the bzrlib version
45-from bzrlib import version_info
46-
47 from bzrlib.lazy_import import lazy_import
48 lazy_import(globals(), """
49 from bzrlib import (
50- branch as _mod_branch,
51 trace,
52 )
53 """)
54
55 from bzrlib.commands import Command, Option, register_command
56+from bzrlib import (
57+ branch as _mod_branch,
58+ lazy_regex,
59+ # Since we are a built-in plugin we share the bzrlib version
60+ version_info,
61+ )
62 from bzrlib.directory_service import directories
63 from bzrlib.errors import (
64 BzrCommandError,
65@@ -283,12 +285,70 @@
66 'Launchpad-based directory service',)
67 _register_directory()
68
69+# This is kept in __init__ so that we don't load lp_api_lite unless the branch
70+# actually matches. That way we can avoid importing extra dependencies like
71+# json.
72+_package_branch = lazy_regex.lazy_compile(
73+ r'bazaar.launchpad.net.*?/'
74+ r'(?P<user>~[^/]+/)?(?P<archive>ubuntu|debian)/(?P<series>[^/]+/)?'
75+ r'(?P<project>[^/]+)(?P<branch>/[^/]+)?'
76+ )
77+
78+def _get_package_branch_info(url):
79+ """Determine the packaging information for this URL.
80+
81+ :return: If this isn't a packaging branch, return None. If it is, return
82+ (archive, series, project)
83+ """
84+ if url is None:
85+ return None
86+ m = _package_branch.search(url)
87+ if m is None:
88+ return None
89+ archive, series, project, user = m.group('archive', 'series',
90+ 'project', 'user')
91+ if series is not None:
92+ # series is optional, so the regex includes the extra '/', we don't
93+ # want to send that on (it causes Internal Server Errors.)
94+ series = series.strip('/')
95+ if user is not None:
96+ user = user.strip('~/')
97+ if user != 'ubuntu-branches':
98+ return None
99+ return archive, series, project
100+
101+
102+def _check_is_up_to_date(the_branch):
103+ info = _get_package_branch_info(the_branch.base)
104+ if info is None:
105+ return
106+ c = the_branch.get_config()
107+ verbosity = c.get_user_option('launchpad.packaging_verbosity')
108+ if verbosity is not None:
109+ verbosity = verbosity.lower()
110+ if verbosity == 'off':
111+ trace.mutter('not checking %s because verbosity is turned off'
112+ % (the_branch.base,))
113+ return
114+ archive, series, project = info
115+ from bzrlib.plugins.launchpad import lp_api_lite
116+ latest_pub = lp_api_lite.LatestPublication(archive, series, project)
117+ lp_api_lite.report_freshness(the_branch, verbosity, latest_pub)
118+
119+
120+def _register_hooks():
121+ _mod_branch.Branch.hooks.install_named_hook('open',
122+ _check_is_up_to_date, 'package-branch-up-to-date')
123+
124+
125+_register_hooks()
126
127 def load_tests(basic_tests, module, loader):
128 testmod_names = [
129 'test_account',
130 'test_register',
131 'test_lp_api',
132+ 'test_lp_api_lite',
133 'test_lp_directory',
134 'test_lp_login',
135 'test_lp_open',
136
137=== added file 'bzrlib/plugins/launchpad/lp_api_lite.py'
138--- bzrlib/plugins/launchpad/lp_api_lite.py 1970-01-01 00:00:00 +0000
139+++ bzrlib/plugins/launchpad/lp_api_lite.py 2011-08-15 10:49:24 +0000
140@@ -0,0 +1,285 @@
141+# Copyright (C) 2011 Canonical Ltd
142+#
143+# This program is free software; you can redistribute it and/or modify
144+# it under the terms of the GNU General Public License as published by
145+# the Free Software Foundation; either version 2 of the License, or
146+# (at your option) any later version.
147+#
148+# This program is distributed in the hope that it will be useful,
149+# but WITHOUT ANY WARRANTY; without even the implied warranty of
150+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
151+# GNU General Public License for more details.
152+#
153+# You should have received a copy of the GNU General Public License
154+# along with this program; if not, write to the Free Software
155+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
156+
157+"""Tools for dealing with the Launchpad API without using launchpadlib.
158+
159+The api itself is a RESTful interface, so we can make HTTP queries directly.
160+loading launchpadlib itself has a fairly high overhead (just calling
161+Launchpad.login_anonymously() takes a 500ms once the WADL is cached, and 5+s to
162+get the WADL.
163+"""
164+
165+try:
166+ # Use simplejson if available, much faster, and can be easily installed in
167+ # older versions of python
168+ import simplejson as json
169+except ImportError:
170+ # Is present since python 2.6
171+ try:
172+ import json
173+ except ImportError:
174+ json = None
175+
176+import time
177+import urllib
178+import urllib2
179+
180+from bzrlib import (
181+ revision,
182+ trace,
183+ )
184+
185+
186+class LatestPublication(object):
187+ """Encapsulate how to find the latest publication for a given project."""
188+
189+ LP_API_ROOT = 'https://api.launchpad.net/1.0'
190+
191+ def __init__(self, archive, series, project):
192+ self._archive = archive
193+ self._project = project
194+ self._setup_series_and_pocket(series)
195+
196+ def _setup_series_and_pocket(self, series):
197+ """Parse the 'series' info into a series and a pocket.
198+
199+ eg::
200+ _setup_series_and_pocket('natty-proposed')
201+ => _series == 'natty'
202+ _pocket == 'Proposed'
203+ """
204+ self._series = series
205+ self._pocket = None
206+ if self._series is not None and '-' in self._series:
207+ self._series, self._pocket = self._series.split('-', 1)
208+ self._pocket = self._pocket.title()
209+ else:
210+ self._pocket = 'Release'
211+
212+ def _archive_URL(self):
213+ """Return the Launchpad 'Archive' URL that we will query.
214+ This is everything in the URL except the query parameters.
215+ """
216+ return '%s/%s/+archive/primary' % (self.LP_API_ROOT, self._archive)
217+
218+ def _publication_status(self):
219+ """Handle the 'status' field.
220+ It seems that Launchpad tracks all 'debian' packages as 'Pending', while
221+ for 'ubuntu' we care about the 'Published' packages.
222+ """
223+ if self._archive == 'debian':
224+ # Launchpad only tracks debian packages as "Pending", it doesn't mark
225+ # them Published
226+ return 'Pending'
227+ return 'Published'
228+
229+ def _query_params(self):
230+ """Get the parameters defining our query.
231+ This defines the actions we are making against the archive.
232+ :return: A dict of query parameters.
233+ """
234+ params = {'ws.op': 'getPublishedSources',
235+ 'exact_match': 'true',
236+ # If we need to use "" shouldn't we quote the project somehow?
237+ 'source_name': '"%s"' % (self._project,),
238+ 'status': self._publication_status(),
239+ # We only need the latest one, the results seem to be properly
240+ # most-recent-debian-version sorted
241+ 'ws.size': '1',
242+ }
243+ if self._series is not None:
244+ params['distro_series'] = '/%s/%s' % (self._archive, self._series)
245+ if self._pocket is not None:
246+ params['pocket'] = self._pocket
247+ return params
248+
249+ def _query_URL(self):
250+ """Create the full URL that we need to query, including parameters."""
251+ params = self._query_params()
252+ # We sort to give deterministic results for testing
253+ encoded = urllib.urlencode(sorted(params.items()))
254+ return '%s?%s' % (self._archive_URL(), encoded)
255+
256+ def _get_lp_info(self):
257+ """Place an actual HTTP query against the Launchpad service."""
258+ if json is None:
259+ return None
260+ query_URL = self._query_URL()
261+ try:
262+ req = urllib2.Request(query_URL)
263+ response = urllib2.urlopen(req)
264+ json_info = response.read()
265+ # TODO: We haven't tested the HTTPError
266+ except (urllib2.URLError, urllib2.HTTPError), e:
267+ trace.mutter('failed to place query to %r' % (query_URL,))
268+ trace.log_exception_quietly()
269+ return None
270+ return json_info
271+
272+ def _parse_json_info(self, json_info):
273+ """Parse the json response from Launchpad into objects."""
274+ if json is None:
275+ return None
276+ try:
277+ return json.loads(json_info)
278+ except Exception:
279+ trace.mutter('Failed to parse json info: %r' % (json_info,))
280+ trace.log_exception_quietly()
281+ return None
282+
283+ def get_latest_version(self):
284+ """Get the latest published version for the given package."""
285+ json_info = self._get_lp_info()
286+ if json_info is None:
287+ return None
288+ info = self._parse_json_info(json_info)
289+ if info is None:
290+ return None
291+ try:
292+ entries = info['entries']
293+ if len(entries) == 0:
294+ return None
295+ return entries[0]['source_package_version']
296+ except KeyError:
297+ trace.log_exception_quietly()
298+ return None
299+
300+ def place(self):
301+ """Text-form for what location this represents.
302+
303+ Example::
304+ ubuntu, natty => Ubuntu Natty
305+ ubuntu, natty-proposed => Ubuntu Natty Proposed
306+ :return: A string representing the location we are checking.
307+ """
308+ place = self._archive
309+ if self._series is not None:
310+ place = '%s %s' % (place, self._series)
311+ if self._pocket is not None and self._pocket != 'Release':
312+ place = '%s %s' % (place, self._pocket)
313+ return place.title()
314+
315+
316+def get_latest_publication(archive, series, project):
317+ """Get the most recent publication for a given project.
318+
319+ :param archive: Either 'ubuntu' or 'debian'
320+ :param series: Something like 'natty', 'sid', etc. Can be set as None. Can
321+ also include a pocket such as 'natty-proposed'.
322+ :param project: Something like 'bzr'
323+ :return: A version string indicating the most-recent version published in
324+ Launchpad. Might return None if there is an error.
325+ """
326+ lp = LatestPublication(archive, series, project)
327+ return lp.get_latest_version()
328+
329+
330+def get_most_recent_tag(tag_dict, the_branch):
331+ """Get the most recent revision that has been tagged."""
332+ # Note: this assumes that a given rev won't get tagged multiple times. But
333+ # it should be valid for the package importer branches that we care
334+ # about
335+ reverse_dict = dict((rev, tag) for tag, rev in tag_dict.iteritems())
336+ the_branch.lock_read()
337+ try:
338+ history = the_branch.repository.iter_reverse_revision_history(
339+ the_branch.last_revision())
340+ for rev_id in history:
341+ if rev_id in reverse_dict:
342+ return reverse_dict[rev_id]
343+ finally:
344+ the_branch.unlock()
345+
346+
347+def _get_newest_versions(the_branch, latest_pub):
348+ """Get information about how 'fresh' this packaging branch is.
349+
350+ :param the_branch: The Branch to check
351+ :param latest_pub: The LatestPublication used to check most recent
352+ published version.
353+ :return: (latest_ver, branch_latest_ver)
354+ """
355+ t = time.time()
356+ latest_ver = latest_pub.get_latest_version()
357+ t_latest_ver = time.time() - t
358+ trace.mutter('LatestPublication.get_latest_version took: %.3fs'
359+ % (t_latest_ver,))
360+ if latest_ver is None:
361+ return None, None
362+ t = time.time()
363+ tags = the_branch.tags.get_tag_dict()
364+ t_tag_dict = time.time() - t
365+ trace.mutter('LatestPublication.get_tag_dict took: %.3fs' % (t_tag_dict,))
366+ if latest_ver in tags:
367+ # branch might have a newer tag, but we don't really care
368+ return latest_ver, latest_ver
369+ else:
370+ best_tag = get_most_recent_tag(tags, the_branch)
371+ return latest_ver, best_tag
372+
373+
374+def _report_freshness(latest_ver, branch_latest_ver, place, verbosity,
375+ report_func):
376+ """Report if the branch is up-to-date."""
377+ if latest_ver is None:
378+ if verbosity == 'all':
379+ report_func('Most recent %s version: MISSING' % (place,))
380+ elif verbosity == 'short':
381+ report_func('%s is MISSING a version' % (place,))
382+ return
383+ elif latest_ver == branch_latest_ver:
384+ if verbosity == 'minimal':
385+ return
386+ elif verbosity == 'short':
387+ report_func('%s is CURRENT in %s' % (latest_ver, place))
388+ else:
389+ report_func('Most recent %s version: %s\n'
390+ 'Packaging branch status: CURRENT'
391+ % (place, latest_ver))
392+ else:
393+ if verbosity in ('minimal', 'short'):
394+ if branch_latest_ver is None:
395+ branch_latest_ver = 'Branch'
396+ report_func('%s is OUT-OF-DATE, %s has %s'
397+ % (branch_latest_ver, place, latest_ver))
398+ else:
399+ report_func('Most recent %s version: %s\n'
400+ 'Packaging branch version: %s\n'
401+ 'Packaging branch status: OUT-OF-DATE'
402+ % (place, latest_ver, branch_latest_ver))
403+
404+
405+def report_freshness(the_branch, verbosity, latest_pub):
406+ """Report to the user how up-to-date the packaging branch is.
407+
408+ :param the_branch: A Branch object
409+ :param verbosity: Can be one of:
410+ off: Do not print anything, and skip all checks.
411+ all: Print all information that we have in a verbose manner, this
412+ includes misses, etc.
413+ short: Print information, but only one-line summaries
414+ minimal: Only print a one-line summary when the package branch is
415+ out-of-date
416+ :param latest_pub: A LatestPublication instance
417+ """
418+ if verbosity == 'off':
419+ return
420+ if verbosity is None:
421+ verbosity = 'all'
422+ latest_ver, branch_ver = _get_newest_versions(the_branch, latest_pub)
423+ place = latest_pub.place()
424+ _report_freshness(latest_ver, branch_ver, place, verbosity,
425+ trace.note)
426
427=== added file 'bzrlib/plugins/launchpad/test_lp_api_lite.py'
428--- bzrlib/plugins/launchpad/test_lp_api_lite.py 1970-01-01 00:00:00 +0000
429+++ bzrlib/plugins/launchpad/test_lp_api_lite.py 2011-08-15 10:49:24 +0000
430@@ -0,0 +1,557 @@
431+# Copyright (C) 2011 Canonical Ltd
432+#
433+# This program is free software; you can redistribute it and/or modify
434+# it under the terms of the GNU General Public License as published by
435+# the Free Software Foundation; either version 2 of the License, or
436+# (at your option) any later version.
437+#
438+# This program is distributed in the hope that it will be useful,
439+# but WITHOUT ANY WARRANTY; without even the implied warranty of
440+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
441+# GNU General Public License for more details.
442+#
443+# You should have received a copy of the GNU General Public License
444+# along with this program; if not, write to the Free Software
445+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
446+
447+"""Tools for dealing with the Launchpad API without using launchpadlib.
448+"""
449+
450+import doctest
451+import socket
452+
453+from bzrlib import tests
454+from bzrlib.plugins import launchpad
455+from bzrlib.plugins.launchpad import lp_api_lite
456+
457+from testtools.matchers import DocTestMatches
458+
459+
460+class _JSONParserFeature(tests.Feature):
461+
462+ def _probe(self):
463+ return lp_api_lite.json is not None
464+
465+ def feature_name(self):
466+ return 'simplejson or json'
467+
468+JSONParserFeature = _JSONParserFeature()
469+
470+_example_response = r"""
471+{
472+ "total_size": 2,
473+ "start": 0,
474+ "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",
475+ "entries": [
476+ {
477+ "package_creator_link": "https://api.launchpad.net/1.0/~maxb",
478+ "package_signer_link": "https://api.launchpad.net/1.0/~jelmer",
479+ "source_package_name": "bzr",
480+ "removal_comment": null,
481+ "display_name": "bzr 2.1.4-0ubuntu1 in lucid",
482+ "date_made_pending": null,
483+ "source_package_version": "2.1.4-0ubuntu1",
484+ "date_superseded": null,
485+ "http_etag": "\"9ba966152dec474dc0fe1629d0bbce2452efaf3b-5f4c3fbb3eaf26d502db4089777a9b6a0537ffab\"",
486+ "self_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/1750327",
487+ "distro_series_link": "https://api.launchpad.net/1.0/ubuntu/lucid",
488+ "component_name": "main",
489+ "status": "Published",
490+ "date_removed": null,
491+ "pocket": "Updates",
492+ "date_published": "2011-05-30T06:09:58.653984+00:00",
493+ "removed_by_link": null,
494+ "section_name": "devel",
495+ "resource_type_link": "https://api.launchpad.net/1.0/#source_package_publishing_history",
496+ "archive_link": "https://api.launchpad.net/1.0/ubuntu/+archive/primary",
497+ "package_maintainer_link": "https://api.launchpad.net/1.0/~ubuntu-devel-discuss-lists",
498+ "date_created": "2011-05-30T05:19:12.233621+00:00",
499+ "scheduled_deletion_date": null
500+ }
501+ ]
502+}"""
503+
504+_no_versions_response = '{"total_size": 0, "start": 0, "entries": []}'
505+
506+
507+class TestLatestPublication(tests.TestCase):
508+
509+ def make_latest_publication(self, archive='ubuntu', series='natty',
510+ project='bzr'):
511+ return lp_api_lite.LatestPublication(archive, series, project)
512+
513+ def assertPlace(self, place, archive, series, project):
514+ lp = lp_api_lite.LatestPublication(archive, series, project)
515+ self.assertEqual(place, lp.place())
516+
517+ def test_init(self):
518+ latest_pub = self.make_latest_publication()
519+ self.assertEqual('ubuntu', latest_pub._archive)
520+ self.assertEqual('natty', latest_pub._series)
521+ self.assertEqual('bzr', latest_pub._project)
522+ self.assertEqual('Release', latest_pub._pocket)
523+
524+ def test__archive_URL(self):
525+ latest_pub = self.make_latest_publication()
526+ self.assertEqual(
527+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary',
528+ latest_pub._archive_URL())
529+
530+ def test__publication_status_for_ubuntu(self):
531+ latest_pub = self.make_latest_publication()
532+ self.assertEqual('Published', latest_pub._publication_status())
533+
534+ def test__publication_status_for_debian(self):
535+ latest_pub = self.make_latest_publication(archive='debian')
536+ self.assertEqual('Pending', latest_pub._publication_status())
537+
538+ def test_pocket(self):
539+ latest_pub = self.make_latest_publication(series='natty-proposed')
540+ self.assertEqual('natty', latest_pub._series)
541+ self.assertEqual('Proposed', latest_pub._pocket)
542+
543+ def test_series_None(self):
544+ latest_pub = self.make_latest_publication(series=None)
545+ self.assertEqual('ubuntu', latest_pub._archive)
546+ self.assertEqual(None, latest_pub._series)
547+ self.assertEqual('bzr', latest_pub._project)
548+ self.assertEqual('Release', latest_pub._pocket)
549+
550+ def test__query_params(self):
551+ latest_pub = self.make_latest_publication()
552+ self.assertEqual({'ws.op': 'getPublishedSources',
553+ 'exact_match': 'true',
554+ 'source_name': '"bzr"',
555+ 'status': 'Published',
556+ 'ws.size': '1',
557+ 'distro_series': '/ubuntu/natty',
558+ 'pocket': 'Release',
559+ }, latest_pub._query_params())
560+
561+ def test__query_params_no_series(self):
562+ latest_pub = self.make_latest_publication(series=None)
563+ self.assertEqual({'ws.op': 'getPublishedSources',
564+ 'exact_match': 'true',
565+ 'source_name': '"bzr"',
566+ 'status': 'Published',
567+ 'ws.size': '1',
568+ 'pocket': 'Release',
569+ }, latest_pub._query_params())
570+
571+ def test__query_params_pocket(self):
572+ latest_pub = self.make_latest_publication(series='natty-proposed')
573+ self.assertEqual({'ws.op': 'getPublishedSources',
574+ 'exact_match': 'true',
575+ 'source_name': '"bzr"',
576+ 'status': 'Published',
577+ 'ws.size': '1',
578+ 'distro_series': '/ubuntu/natty',
579+ 'pocket': 'Proposed',
580+ }, latest_pub._query_params())
581+
582+ def test__query_URL(self):
583+ latest_pub = self.make_latest_publication()
584+ # we explicitly sort params, so we can be sure this URL matches exactly
585+ self.assertEqual(
586+ 'https://api.launchpad.net/1.0/ubuntu/+archive/primary'
587+ '?distro_series=%2Fubuntu%2Fnatty&exact_match=true'
588+ '&pocket=Release&source_name=%22bzr%22&status=Published'
589+ '&ws.op=getPublishedSources&ws.size=1',
590+ latest_pub._query_URL())
591+
592+ def DONT_test__gracefully_handle_failed_rpc_connection(self):
593+ # TODO: This test kind of sucks. We intentionally create an arbitrary
594+ # port and don't listen to it, because we want the request to fail.
595+ # However, it seems to take 1s for it to timeout. Is there a way
596+ # to make it fail faster?
597+ latest_pub = self.make_latest_publication()
598+ s = socket.socket()
599+ s.bind(('127.0.0.1', 0))
600+ addr, port = s.getsockname()
601+ latest_pub.LP_API_ROOT = 'http://%s:%s/' % (addr, port)
602+ s.close()
603+ self.assertIs(None, latest_pub._get_lp_info())
604+
605+ def DONT_test__query_launchpad(self):
606+ # TODO: This is a test that we are making a valid request against
607+ # launchpad. This seems important, but it is slow, requires net
608+ # access, and requires launchpad to be up and running. So for
609+ # now, it is commented out for production tests.
610+ latest_pub = self.make_latest_publication()
611+ json_txt = latest_pub._get_lp_info()
612+ self.assertIsNot(None, json_txt)
613+ if lp_api_lite.json is None:
614+ # We don't have a way to parse the text
615+ return
616+ # The content should be a valid json result
617+ content = lp_api_lite.json.loads(json_txt)
618+ entries = content['entries'] # It should have an 'entries' field.
619+ # ws.size should mean we get 0 or 1, and there should be something
620+ self.assertEqual(1, len(entries))
621+ entry = entries[0]
622+ self.assertEqual('bzr', entry['source_package_name'])
623+ version = entry['source_package_version']
624+ self.assertIsNot(None, version)
625+
626+ def disableJSON(self):
627+ orig = lp_api_lite.json
628+ def cleanup():
629+ lp_api_lite.json = orig
630+ self.addCleanup(cleanup)
631+ lp_api_lite.json = None
632+
633+ def test__get_lp_info_no_json(self):
634+ # If we can't parse the json, we don't make the query.
635+ self.disableJSON()
636+ latest_pub = self.make_latest_publication()
637+ self.assertIs(None, latest_pub._get_lp_info())
638+
639+ def test__parse_json_info_no_module(self):
640+ # If a json parsing module isn't available, we just return None here.
641+ self.disableJSON()
642+ latest_pub = self.make_latest_publication()
643+ self.assertIs(None, latest_pub._parse_json_info(_example_response))
644+
645+ def test__parse_json_example_response(self):
646+ self.requireFeature(JSONParserFeature)
647+ latest_pub = self.make_latest_publication()
648+ content = latest_pub._parse_json_info(_example_response)
649+ self.assertIsNot(None, content)
650+ self.assertEqual(2, content['total_size'])
651+ entries = content['entries']
652+ self.assertEqual(1, len(entries))
653+ entry = entries[0]
654+ self.assertEqual('bzr', entry['source_package_name'])
655+ self.assertEqual("2.1.4-0ubuntu1", entry["source_package_version"])
656+
657+ def test__parse_json_not_json(self):
658+ self.requireFeature(JSONParserFeature)
659+ latest_pub = self.make_latest_publication()
660+ self.assertIs(None, latest_pub._parse_json_info('Not_valid_json'))
661+
662+ def test_get_latest_version_no_response(self):
663+ latest_pub = self.make_latest_publication()
664+ latest_pub._get_lp_info = lambda: None
665+ self.assertEqual(None, latest_pub.get_latest_version())
666+
667+ def test_get_latest_version_no_json(self):
668+ self.disableJSON()
669+ latest_pub = self.make_latest_publication()
670+ self.assertEqual(None, latest_pub.get_latest_version())
671+
672+ def test_get_latest_version_invalid_json(self):
673+ self.requireFeature(JSONParserFeature)
674+ latest_pub = self.make_latest_publication()
675+ latest_pub._get_lp_info = lambda: "not json"
676+ self.assertEqual(None, latest_pub.get_latest_version())
677+
678+ def test_get_latest_version_no_versions(self):
679+ self.requireFeature(JSONParserFeature)
680+ latest_pub = self.make_latest_publication()
681+ latest_pub._get_lp_info = lambda: _no_versions_response
682+ self.assertEqual(None, latest_pub.get_latest_version())
683+
684+ def test_get_latest_version_missing_entries(self):
685+ # Launchpad's no-entries response does have an empty entries value.
686+ # However, lets test that we handle other failures without tracebacks
687+ self.requireFeature(JSONParserFeature)
688+ latest_pub = self.make_latest_publication()
689+ latest_pub._get_lp_info = lambda: '{}'
690+ self.assertEqual(None, latest_pub.get_latest_version())
691+
692+ def test_get_latest_version_invalid_entries(self):
693+ # Make sure we sanely handle a json response we don't understand
694+ self.requireFeature(JSONParserFeature)
695+ latest_pub = self.make_latest_publication()
696+ latest_pub._get_lp_info = lambda: '{"entries": {"a": 1}}'
697+ self.assertEqual(None, latest_pub.get_latest_version())
698+
699+ def test_get_latest_version_example(self):
700+ self.requireFeature(JSONParserFeature)
701+ latest_pub = self.make_latest_publication()
702+ latest_pub._get_lp_info = lambda: _example_response
703+ self.assertEqual("2.1.4-0ubuntu1", latest_pub.get_latest_version())
704+
705+ def DONT_test_get_latest_version_from_launchpad(self):
706+ self.requireFeature(JSONParserFeature)
707+ latest_pub = self.make_latest_publication()
708+ self.assertIsNot(None, latest_pub.get_latest_version())
709+
710+ def test_place(self):
711+ self.assertPlace('Ubuntu', 'ubuntu', None, 'bzr')
712+ self.assertPlace('Ubuntu Natty', 'ubuntu', 'natty', 'bzr')
713+ self.assertPlace('Ubuntu Natty Proposed', 'ubuntu', 'natty-proposed',
714+ 'bzr')
715+ self.assertPlace('Debian', 'debian', None, 'bzr')
716+ self.assertPlace('Debian Sid', 'debian', 'sid', 'bzr')
717+
718+
719+class TestIsUpToDate(tests.TestCase):
720+
721+ def assertPackageBranchRe(self, url, user, archive, series, project):
722+ m = launchpad._package_branch.search(url)
723+ if m is None:
724+ self.fail('package_branch regex did not match url: %s' % (url,))
725+ self.assertEqual(
726+ (user, archive, series, project),
727+ m.group('user', 'archive', 'series', 'project'))
728+
729+ def assertNotPackageBranch(self, url):
730+ self.assertIs(None, launchpad._get_package_branch_info(url))
731+
732+ def assertBranchInfo(self, url, archive, series, project):
733+ self.assertEqual((archive, series, project),
734+ launchpad._get_package_branch_info(url))
735+
736+ def test_package_branch_regex(self):
737+ self.assertPackageBranchRe(
738+ 'http://bazaar.launchpad.net/+branch/ubuntu/foo',
739+ None, 'ubuntu', None, 'foo')
740+ self.assertPackageBranchRe(
741+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo',
742+ None, 'ubuntu', 'natty/', 'foo')
743+ self.assertPackageBranchRe(
744+ 'sftp://bazaar.launchpad.net/+branch/debian/foo',
745+ None, 'debian', None, 'foo')
746+ self.assertPackageBranchRe(
747+ 'http://bazaar.launchpad.net/+branch/debian/sid/foo',
748+ None, 'debian', 'sid/', 'foo')
749+ self.assertPackageBranchRe(
750+ 'http://bazaar.launchpad.net/+branch'
751+ '/~ubuntu-branches/ubuntu/natty/foo/natty',
752+ '~ubuntu-branches/', 'ubuntu', 'natty/', 'foo')
753+ self.assertPackageBranchRe(
754+ 'http://bazaar.launchpad.net/+branch'
755+ '/~user/ubuntu/natty/foo/test',
756+ '~user/', 'ubuntu', 'natty/', 'foo')
757+
758+ def test_package_branch_doesnt_match(self):
759+ self.assertNotPackageBranch('http://example.com/ubuntu/foo')
760+ self.assertNotPackageBranch(
761+ 'http://bazaar.launchpad.net/+branch/bzr')
762+ self.assertNotPackageBranch(
763+ 'http://bazaar.launchpad.net/+branch/~bzr-pqm/bzr/bzr.dev')
764+ # Not a packaging branch because ~user isn't ~ubuntu-branches
765+ self.assertNotPackageBranch(
766+ 'http://bazaar.launchpad.net/+branch'
767+ '/~user/ubuntu/natty/foo/natty')
768+ # Older versions of bzr-svn/hg/git did not set Branch.base until after
769+ # they called Branch.__init__().
770+ self.assertNotPackageBranch(None)
771+
772+ def test__get_package_branch_info(self):
773+ self.assertBranchInfo(
774+ 'bzr+ssh://bazaar.launchpad.net/+branch/ubuntu/natty/foo',
775+ 'ubuntu', 'natty', 'foo')
776+ self.assertBranchInfo(
777+ 'bzr+ssh://bazaar.launchpad.net/+branch'
778+ '/~ubuntu-branches/ubuntu/natty/foo/natty',
779+ 'ubuntu', 'natty', 'foo')
780+ self.assertBranchInfo(
781+ 'http://bazaar.launchpad.net/+branch'
782+ '/~ubuntu-branches/debian/sid/foo/sid',
783+ 'debian', 'sid', 'foo')
784+
785+
786+class TestGetMostRecentTag(tests.TestCaseWithMemoryTransport):
787+
788+ def make_simple_builder(self):
789+ builder = self.make_branch_builder('tip')
790+ builder.build_snapshot('A', None, [
791+ ('add', ('', 'root-id', 'directory', None))])
792+ b = builder.get_branch()
793+ b.tags.set_tag('tip-1.0', 'A')
794+ return builder, b, b.tags.get_tag_dict()
795+
796+ def test_get_most_recent_tag_tip(self):
797+ builder, b, tag_dict = self.make_simple_builder()
798+ self.assertEqual('tip-1.0',
799+ lp_api_lite.get_most_recent_tag(tag_dict, b))
800+
801+ def test_get_most_recent_tag_older(self):
802+ builder, b, tag_dict = self.make_simple_builder()
803+ builder.build_snapshot('B', ['A'], [])
804+ self.assertEqual('B', b.last_revision())
805+ self.assertEqual('tip-1.0',
806+ lp_api_lite.get_most_recent_tag(tag_dict, b))
807+
808+
809+class StubLatestPublication(object):
810+
811+ def __init__(self, latest):
812+ self.called = False
813+ self.latest = latest
814+
815+ def get_latest_version(self):
816+ self.called = True
817+ return self.latest
818+
819+ def place(self):
820+ return 'Ubuntu Natty'
821+
822+
823+class TestReportFreshness(tests.TestCaseWithMemoryTransport):
824+
825+ def setUp(self):
826+ super(TestReportFreshness, self).setUp()
827+ builder = self.make_branch_builder('tip')
828+ builder.build_snapshot('A', None, [
829+ ('add', ('', 'root-id', 'directory', None))])
830+ self.branch = builder.get_branch()
831+
832+ def assertFreshnessReports(self, verbosity, latest_version, content):
833+ """Assert that lp_api_lite.report_freshness reports the given content.
834+
835+ :param verbosity: The reporting level
836+ :param latest_version: The version reported by StubLatestPublication
837+ :param content: The expected content. This should be in DocTest form.
838+ """
839+ orig_log_len = len(self.get_log())
840+ lp_api_lite.report_freshness(self.branch, verbosity,
841+ StubLatestPublication(latest_version))
842+ new_content = self.get_log()[orig_log_len:]
843+ # Strip out lines that have LatestPublication.get_* because those are
844+ # timing related lines. While interesting to log for now, they aren't
845+ # something we want to be testing
846+ new_content = new_content.split('\n')
847+ for i in range(2):
848+ if (len(new_content) > 0
849+ and 'LatestPublication.get_' in new_content[0]):
850+ new_content = new_content[1:]
851+ new_content = '\n'.join(new_content)
852+ self.assertThat(new_content,
853+ DocTestMatches(content,
854+ doctest.ELLIPSIS | doctest.REPORT_UDIFF))
855+
856+ def test_verbosity_off_skips_check(self):
857+ # We force _get_package_branch_info so that we know it would otherwise
858+ # try to connect to launcphad
859+ orig_gpbi = launchpad._get_package_branch_info
860+ orig_lp = lp_api_lite.LatestPublication
861+ def cleanup():
862+ launchpad._get_package_branch_info = orig_gpbi
863+ lp_api_lite.LatestPublication = orig_lp
864+ self.addCleanup(cleanup)
865+ launchpad._get_package_branch_info = lambda x: ('ubuntu', 'natty', 'bzr')
866+ lp_api_lite.LatestPublication = lambda *args: self.fail('Tried to query launchpad')
867+ c = self.branch.get_config()
868+ c.set_user_option('launchpad.packaging_verbosity', 'off')
869+ orig_log_len = len(self.get_log())
870+ launchpad._check_is_up_to_date(self.branch)
871+ new_content = self.get_log()[orig_log_len:]
872+ self.assertContainsRe(new_content,
873+ 'not checking memory.*/tip/ because verbosity is turned off')
874+
875+ def test_verbosity_off(self):
876+ latest_pub = StubLatestPublication('1.0-1ubuntu2')
877+ lp_api_lite.report_freshness(self.branch, 'off', latest_pub)
878+ self.assertFalse(latest_pub.called)
879+
880+ def test_verbosity_all_out_of_date_smoke(self):
881+ self.branch.tags.set_tag('1.0-1ubuntu1', 'A')
882+ self.assertFreshnessReports('all', '1.0-1ubuntu2',
883+ ' INFO Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
884+ 'Packaging branch version: 1.0-1ubuntu1\n'
885+ 'Packaging branch status: OUT-OF-DATE\n')
886+
887+
888+class Test_GetNewestVersions(tests.TestCaseWithMemoryTransport):
889+
890+ def setUp(self):
891+ super(Test_GetNewestVersions, self).setUp()
892+ builder = self.make_branch_builder('tip')
893+ builder.build_snapshot('A', None, [
894+ ('add', ('', 'root-id', 'directory', None))])
895+ self.branch = builder.get_branch()
896+
897+ def assertLatestVersions(self, latest_branch_version, pub_version):
898+ if latest_branch_version is not None:
899+ self.branch.tags.set_tag(latest_branch_version, 'A')
900+ latest_pub = StubLatestPublication(pub_version)
901+ self.assertEqual((pub_version, latest_branch_version),
902+ lp_api_lite._get_newest_versions(self.branch, latest_pub))
903+
904+ def test_no_tags(self):
905+ self.assertLatestVersions(None, '1.0-1ubuntu2')
906+
907+ def test_out_of_date(self):
908+ self.assertLatestVersions('1.0-1ubuntu1', '1.0-1ubuntu2')
909+
910+ def test_up_to_date(self):
911+ self.assertLatestVersions('1.0-1ubuntu2', '1.0-1ubuntu2')
912+
913+ def test_missing(self):
914+ self.assertLatestVersions(None, None)
915+
916+
917+class Test_ReportFreshness(tests.TestCase):
918+
919+ def assertReportedFreshness(self, verbosity, latest_ver, branch_latest_ver,
920+ content, place='Ubuntu Natty'):
921+ """Assert that lp_api_lite.report_freshness reports the given content.
922+ """
923+ reported = []
924+ def report_func(value):
925+ reported.append(value)
926+ lp_api_lite._report_freshness(latest_ver, branch_latest_ver, place,
927+ verbosity, report_func)
928+ new_content = '\n'.join(reported)
929+ self.assertThat(new_content,
930+ DocTestMatches(content,
931+ doctest.ELLIPSIS | doctest.REPORT_UDIFF))
932+
933+ def test_verbosity_minimal_no_tags(self):
934+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', None,
935+ 'Branch is OUT-OF-DATE, Ubuntu Natty has 1.0-1ubuntu2\n')
936+
937+ def test_verbosity_minimal_out_of_date(self):
938+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu1',
939+ '1.0-1ubuntu1 is OUT-OF-DATE,'
940+ ' Ubuntu Natty has 1.0-1ubuntu2\n')
941+
942+ def test_verbosity_minimal_up_to_date(self):
943+ self.assertReportedFreshness('minimal', '1.0-1ubuntu2', '1.0-1ubuntu2',
944+ '')
945+
946+ def test_verbosity_minimal_missing(self):
947+ self.assertReportedFreshness('minimal', None, None,
948+ '')
949+
950+ def test_verbosity_short_out_of_date(self):
951+ self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu1',
952+ '1.0-1ubuntu1 is OUT-OF-DATE,'
953+ ' Ubuntu Natty has 1.0-1ubuntu2\n')
954+
955+ def test_verbosity_short_up_to_date(self):
956+ self.assertReportedFreshness('short', '1.0-1ubuntu2', '1.0-1ubuntu2',
957+ '1.0-1ubuntu2 is CURRENT in Ubuntu Natty')
958+
959+ def test_verbosity_short_missing(self):
960+ self.assertReportedFreshness('short', None, None,
961+ 'Ubuntu Natty is MISSING a version')
962+
963+ def test_verbosity_all_no_tags(self):
964+ self.assertReportedFreshness('all', '1.0-1ubuntu2', None,
965+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
966+ 'Packaging branch version: None\n'
967+ 'Packaging branch status: OUT-OF-DATE\n')
968+
969+ def test_verbosity_all_out_of_date(self):
970+ self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu1',
971+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
972+ 'Packaging branch version: 1.0-1ubuntu1\n'
973+ 'Packaging branch status: OUT-OF-DATE\n')
974+
975+ def test_verbosity_all_up_to_date(self):
976+ self.assertReportedFreshness('all', '1.0-1ubuntu2', '1.0-1ubuntu2',
977+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
978+ 'Packaging branch status: CURRENT\n')
979+
980+ def test_verbosity_all_missing(self):
981+ self.assertReportedFreshness('all', None, None,
982+ 'Most recent Ubuntu Natty version: MISSING\n')
983+
984+ def test_verbosity_None_is_all(self):
985+ self.assertReportedFreshness(None, '1.0-1ubuntu2', '1.0-1ubuntu2',
986+ 'Most recent Ubuntu Natty version: 1.0-1ubuntu2\n'
987+ 'Packaging branch status: CURRENT\n')

Subscribers

People subscribed via source and target branches