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

Proposed by John A Meinel
Status: Merged
Approved by: John A Meinel
Approved revision: no longer in the source branch.
Merged at revision: 6024
Proposed branch: lp:~jameinel/bzr/2.4-launchpad-package-freshness-609187
Merge into: lp:bzr/2.4
Diff against target: 974 lines (+917/-5)
4 files modified
bzrlib/plugins/launchpad/__init__.py (+63/-5)
bzrlib/plugins/launchpad/lp_api_lite.py (+286/-0)
bzrlib/plugins/launchpad/test_lp_api_lite.py (+543/-0)
doc/en/release-notes/bzr-2.4.txt (+25/-0)
To merge this branch: bzr merge lp:~jameinel/bzr/2.4-launchpad-package-freshness-609187
Reviewer Review Type Date Requested Status
Andrew Bennetts Approve
Review via email: mp+69218@code.launchpad.net

Commit message

Bug #609187, check that packaging import branches are up-to-date when accessing them.

Description of the change

This backports my bug #609187 fix now that we've settled on the output.

So it will check if "lp:ubuntu/*" and "lp:debian/*" are up-to-date, which can be disabled with the config setting, and the verbosity level adjusted.

To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

I haven't read this patch closely, but this is just a direct backport of the patch that landed on lp:bzr, right? In that case, I say: do it!

review: Approve
Revision history for this message
Andrew Bennetts (spiv) wrote :

Although it occurs to me that the text you add to release-notes would make a useful help topic.

(Also, at a glance the ReST markup of that release-notes entry doesn't look like it'll render the way you intend.)

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

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

On 7/27/2011 9:11 AM, Andrew Bennetts wrote:
> Although it occurs to me that the text you add to release-notes would make a useful help topic.
>
> (Also, at a glance the ReST markup of that release-notes entry doesn't look like it'll render the way you intend.)

How do you think I intend it? I was going for a Definition List:
http://docutils.sourceforge.net/docs/user/rst/quickref.html#definition-lists

Which seems to be how it is formatted, though I was expecting the
headers to be bolded, but it is showing up as <dd> and <dt>.

Do you have a different recommendation?

John
=:->

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

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

Subscribers

People subscribed via source and target branches