Merge lp:~mwhudson/launchpad/branching-ubuntu into lp:launchpad

Proposed by Michael Hudson-Doyle
Status: Merged
Merged at revision: not available
Proposed branch: lp:~mwhudson/launchpad/branching-ubuntu
Merge into: lp:launchpad
Diff against target: 1129 lines
4 files modified
database/schema/security.cfg (+16/-0)
lib/lp/codehosting/branchdistro.py (+364/-0)
lib/lp/codehosting/tests/test_branchdistro.py (+685/-0)
scripts/branch-distro.py (+39/-0)
To merge this branch: bzr merge lp:~mwhudson/launchpad/branching-ubuntu
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+13269@code.launchpad.net

Commit message

A script to open a new distribution for branch-based development -- see https://dev.launchpad.net/BranchingUbuntu.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hey Jono,

Finally I think this beast of a branch is ready (actually it's "only" 1123 lines, not too far over the limit :-).

It adds a script that carries out the procedure on https://dev.launchpad.net/BranchingUbuntu. It also adds a --check flag that verifies that a previous run of the script went ok. This was the only way I could convince myself to stop worrying about what would happen if the script crashed or was interrupted -- the idea is that if the script is interrupted we'll run the script again until it completes, then run branch-distro --check to find any damage that was left behind by the interrupted run.

I hope it makes sense, we've talked about this enough!

Cheers,
mwh

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Oh, I guess I should say that we need to test this as much as we can on staging, I think this should be possible.

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (53.2 KiB)

Hey Michael,

Not much to say here, given that we've already talked about it. I look forward to seeing this QA'd on staging.

I have a few comments, one or two of which must be fixed before landing. Other than that, looks good. Thanks for getting this done.

jml

> === modified file 'database/schema/security.cfg'
> --- database/schema/security.cfg 2009-10-05 15:07:10 +0000
> +++ database/schema/security.cfg 2009-10-13 09:11:15 +0000
> @@ -641,6 +641,22 @@
> # Merge notifications
> public.codereviewvote = SELECT
>
> +[branch-distro]
> +type=user
> +public.branch = SELECT, INSERT
> +public.branchsubscription = SELECT, INSERT
> +public.distribution = SELECT
> +public.distroseries = SELECT
> +public.karma = SELECT, INSERT
> +public.karmaaction = SELECT
> +public.person = SELECT
> +public.product = SELECT
> +public.seriessourcepackagebranch = SELECT, INSERT, DELETE
> +public.sourcepackagename = SELECT
> +public.teamparticipation = SELECT
> +public.validpersoncache = SELECT
> +
> +

How did you determine these? Are you sure you've got everything?

> [targetnamecacheupdater]
> type=user
> groups=script
>

> === added file 'lib/lp/codehosting/branchdistro.py'
> --- lib/lp/codehosting/branchdistro.py 1970-01-01 00:00:00 +0000
> +++ lib/lp/codehosting/branchdistro.py 2009-10-13 09:11:15 +0000
> @@ -0,0 +1,355 @@
> +# Copyright 2009 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Opening a new DistroSeries for branch based development.
> +
> +Intended to be run just after a new distro series has been completed, this
> +script will create an official package branch in the new series for every one
> +in the old. The old branch will become stacked on the new, to avoid a using
> +too much disk space whilst retaining best performance for the new branch.
> +"""
> +
> +import os
> +
> +from bzrlib.branch import Branch
> +from bzrlib.bzrdir import BzrDir
> +from bzrlib.errors import NotBranchError, NotStacked
> +
> +from lazr.uri import URI
> +
> +import transaction
> +
> +from zope.component import getUtility
> +
> +from canonical.launchpad.interfaces import ILaunchpadCelebrities
> +from canonical.config import config
> +
> +from lp.code.interfaces.branchcollection import IAllBranches
> +from lp.code.interfaces.branch import BranchExists
> +from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
> +from lp.code.interfaces.seriessourcepackagebranch import (
> + IFindOfficialBranchLinks)
> +from lp.code.enums import BranchType
> +from lp.codehosting.vfs import branch_id_to_path
> +from lp.registry.interfaces.distribution import IDistributionSet
> +from lp.registry.interfaces.pocket import PackagePublishingPocket
> +
> +
> +__metaclass__ = type
> +__all__ = []
> +

These should be above the imports & below the docstring, IIRC.

> +
> +def switch_branches(prefix, scheme, old_db_branch, new_db_branch):
> + """Move bzr data from an old to a new branch, leaving old stacked on new.
> +
> + T...

review: Approve
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (56.0 KiB)

Jonathan Lange wrote:
> Review: Approve
> Hey Michael,
>
> Not much to say here, given that we've already talked about it. I look forward to seeing this QA'd on staging.
>
> I have a few comments, one or two of which must be fixed before landing. Other than that, looks good. Thanks for getting this done.
>
> jml
>
>> === modified file 'database/schema/security.cfg'
>> --- database/schema/security.cfg 2009-10-05 15:07:10 +0000
>> +++ database/schema/security.cfg 2009-10-13 09:11:15 +0000
>> @@ -641,6 +641,22 @@
>> # Merge notifications
>> public.codereviewvote = SELECT
>>
>> +[branch-distro]
>> +type=user
>> +public.branch = SELECT, INSERT
>> +public.branchsubscription = SELECT, INSERT
>> +public.distribution = SELECT
>> +public.distroseries = SELECT
>> +public.karma = SELECT, INSERT
>> +public.karmaaction = SELECT
>> +public.person = SELECT
>> +public.product = SELECT
>> +public.seriessourcepackagebranch = SELECT, INSERT, DELETE
>> +public.sourcepackagename = SELECT
>> +public.teamparticipation = SELECT
>> +public.validpersoncache = SELECT
>> +
>> +
>
> How did you determine these?

The tests run as this user, so I ran make schema over and over and over
again whilst gradually increasing the permissions...

> Are you sure you've got everything?

Reasonably. As you noticed, I spent a long time writing tests...

>> [targetnamecacheupdater]
>> type=user
>> groups=script
>>
>
>
>> === added file 'lib/lp/codehosting/branchdistro.py'
>> --- lib/lp/codehosting/branchdistro.py 1970-01-01 00:00:00 +0000
>> +++ lib/lp/codehosting/branchdistro.py 2009-10-13 09:11:15 +0000
>> @@ -0,0 +1,355 @@
>> +# Copyright 2009 Canonical Ltd. This software is licensed under the
>> +# GNU Affero General Public License version 3 (see the file LICENSE).
>> +
>> +"""Opening a new DistroSeries for branch based development.
>> +
>> +Intended to be run just after a new distro series has been completed, this
>> +script will create an official package branch in the new series for every one
>> +in the old. The old branch will become stacked on the new, to avoid a using
>> +too much disk space whilst retaining best performance for the new branch.
>> +"""
>> +
>> +import os
>> +
>> +from bzrlib.branch import Branch
>> +from bzrlib.bzrdir import BzrDir
>> +from bzrlib.errors import NotBranchError, NotStacked
>> +
>> +from lazr.uri import URI
>> +
>> +import transaction
>> +
>> +from zope.component import getUtility
>> +
>> +from canonical.launchpad.interfaces import ILaunchpadCelebrities
>> +from canonical.config import config
>> +
>> +from lp.code.interfaces.branchcollection import IAllBranches
>> +from lp.code.interfaces.branch import BranchExists
>> +from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
>> +from lp.code.interfaces.seriessourcepackagebranch import (
>> + IFindOfficialBranchLinks)
>> +from lp.code.enums import BranchType
>> +from lp.codehosting.vfs import branch_id_to_path
>> +from lp.registry.interfaces.distribution import IDistributionSet
>> +from lp.registry.interfa...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2009-10-05 15:07:10 +0000
3+++ database/schema/security.cfg 2009-10-14 00:31:13 +0000
4@@ -641,6 +641,22 @@
5 # Merge notifications
6 public.codereviewvote = SELECT
7
8+[branch-distro]
9+type=user
10+public.branch = SELECT, INSERT
11+public.branchsubscription = SELECT, INSERT
12+public.distribution = SELECT
13+public.distroseries = SELECT
14+public.karma = SELECT, INSERT
15+public.karmaaction = SELECT
16+public.person = SELECT
17+public.product = SELECT
18+public.seriessourcepackagebranch = SELECT, INSERT, DELETE
19+public.sourcepackagename = SELECT
20+public.teamparticipation = SELECT
21+public.validpersoncache = SELECT
22+
23+
24 [targetnamecacheupdater]
25 type=user
26 groups=script
27
28=== added file 'lib/lp/codehosting/branchdistro.py'
29--- lib/lp/codehosting/branchdistro.py 1970-01-01 00:00:00 +0000
30+++ lib/lp/codehosting/branchdistro.py 2009-10-14 00:31:13 +0000
31@@ -0,0 +1,364 @@
32+# Copyright 2009 Canonical Ltd. This software is licensed under the
33+# GNU Affero General Public License version 3 (see the file LICENSE).
34+
35+"""Opening a new DistroSeries for branch based development.
36+
37+Intended to be run just after a new distro series has been completed, this
38+script will create an official package branch in the new series for every one
39+in the old. The old branch will become stacked on the new, to avoid a using
40+too much disk space whilst retaining best performance for the new branch.
41+"""
42+
43+__metaclass__ = type
44+__all__ = [
45+ 'DistroBrancher',
46+ 'switch_branches',
47+ ]
48+
49+import os
50+
51+from bzrlib.branch import Branch
52+from bzrlib.bzrdir import BzrDir
53+from bzrlib.errors import NotBranchError, NotStacked
54+
55+from lazr.uri import URI
56+
57+import transaction
58+
59+from zope.component import getUtility
60+
61+from canonical.launchpad.interfaces import ILaunchpadCelebrities
62+from canonical.config import config
63+
64+from lp.code.interfaces.branchcollection import IAllBranches
65+from lp.code.interfaces.branch import BranchExists
66+from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
67+from lp.code.interfaces.seriessourcepackagebranch import (
68+ IFindOfficialBranchLinks)
69+from lp.code.enums import BranchType
70+from lp.codehosting.vfs import branch_id_to_path
71+from lp.registry.interfaces.distribution import IDistributionSet
72+from lp.registry.interfaces.pocket import PackagePublishingPocket
73+
74+
75+def switch_branches(prefix, scheme, old_db_branch, new_db_branch):
76+ """Move bzr data from an old to a new branch, leaving old stacked on new.
77+
78+ This function is intended to be used just after Ubuntu is released to
79+ create (at the bzr level) a new trunk branch for a source package for the
80+ next release of the distribution. We move the bzr data to the location
81+ for the new branch and replace the trunk branch for the just released
82+ version with a stacked branch pointing at the new branch.
83+
84+ The procedure is to complicated to be carried out atomically, so if this
85+ function is interrupted things may be a little inconsistent (e.g. there
86+ might be a branch in the old location, but not stacked on the new location
87+ yet). There should be no data loss though.
88+
89+ :param prefix: The non-branch id dependent part of the physical path to
90+ the branches on disk.
91+ :param scheme: The branches should be open-able at a URL of the form
92+ ``scheme + :/// + unique_name``.
93+ :param old_db_branch: The branch that currently has the trunk bzr data.
94+ :param old_db_branch: The new trunk branch. This should not have any
95+ presence on disk yet.
96+ """
97+ # Move .bzr directory from old to new location, crashing through the
98+ # abstraction we usually hide our branch locations behind.
99+ old_underlying_path = os.path.join(
100+ prefix, branch_id_to_path(old_db_branch.id))
101+ new_underlying_path = os.path.join(
102+ prefix, branch_id_to_path(new_db_branch.id))
103+ os.makedirs(new_underlying_path)
104+ os.rename(
105+ os.path.join(old_underlying_path, '.bzr'),
106+ os.path.join(new_underlying_path, '.bzr'))
107+
108+ # Create branch at old location -- we use the "clone('null:')" trick to
109+ # preserve the format. We have to open at the logical, unique_name-based,
110+ # location so that it works to set the stacked on url to '/' + a
111+ # unique_name.
112+ new_location_bzrdir = BzrDir.open(
113+ str(URI(
114+ scheme=scheme, host='', path='/' + new_db_branch.unique_name)))
115+ old_location_bzrdir = new_location_bzrdir.clone(
116+ str(URI(
117+ scheme=scheme, host='', path='/' + old_db_branch.unique_name)),
118+ revision_id='null:')
119+
120+ # Set the stacked on url for old location.
121+ old_location_branch = old_location_bzrdir.open_branch()
122+ old_location_branch.set_stacked_on_url('/' + new_db_branch.unique_name)
123+
124+ # Pull from new location to old -- this won't actually transfer any
125+ # revisions, just update the last revision pointer.
126+ old_location_branch.pull(new_location_bzrdir.open_branch())
127+
128+
129+class DistroBrancher:
130+ """Open a new distroseries for branch based development.
131+
132+ `makeNewBranches` will create an official package branch in the new series
133+ for every one in the old. `checkNewBranches` will check that a previous
134+ run of this script completed successfully -- this is only likely to be
135+ really useful if a script run died halfway through or had to be killed.
136+ """
137+
138+ def __init__(self, logger, old_distroseries, new_distroseries):
139+ """Construct a `DistroBrancher`.
140+
141+ The old and new distroseries must be from the same distribution, but
142+ not the same distroseries.
143+
144+ :param logger: A Logger. Problems will be logged to this object at
145+ the WARNING level or higher; progress reports will be logged at
146+ the DEBUG level.
147+ :param old_distroseries: The distroseries that will be examined to
148+ find existing source package branches.
149+ :param new_distroseries: The distroseries that will have new official
150+ source branches made for it.
151+ """
152+ self.logger = logger
153+ if old_distroseries.distribution != new_distroseries.distribution:
154+ raise AssertionError(
155+ "%s and %s are from different distributions!" %
156+ (old_distroseries, new_distroseries))
157+ if old_distroseries == new_distroseries:
158+ raise AssertionError(
159+ "New and old distributions must be different!")
160+ self.old_distroseries = old_distroseries
161+ self.new_distroseries = new_distroseries
162+
163+ @classmethod
164+ def fromNames(cls, logger, distribution_name, old_distroseries_name,
165+ new_distroseries_name):
166+ """Make a `DistroBrancher` from the names of a distro and two series.
167+ """
168+ distribution = getUtility(IDistributionSet).getByName(
169+ distribution_name)
170+ new_distroseries = distribution.getSeries(new_distroseries_name)
171+ old_distroseries = distribution.getSeries(old_distroseries_name)
172+ return cls(logger, old_distroseries, new_distroseries)
173+
174+ def _existingOfficialBranches(self):
175+ """Return the collection of official branches in the old distroseries.
176+ """
177+ branches = getUtility(IAllBranches)
178+ distroseries_branches = branches.inDistroSeries(self.old_distroseries)
179+ return distroseries_branches.officialBranches().getBranches()
180+
181+ def checkConsistentOfficialPackageBranch(self, db_branch):
182+ """Check that `db_branch` is a consistent official package branch.
183+
184+ 'Consistent official package branch' means:
185+
186+ * It's a package branch (rather than a personal or junk branch).
187+ * It's official for its SourcePackage and no other.
188+
189+ This function simply returns True or False -- any problems will be
190+ logged to ``self.logger``.
191+
192+ :param db_branch: The `IBranch` to check.
193+ :return: ``True`` if the branch is a consistent official package
194+ branch, ``False`` otherwise.
195+ """
196+ if db_branch.product:
197+ self.logger.warning(
198+ "Encountered unexpected product branch %r",
199+ db_branch.unique_name)
200+ return False
201+ if not db_branch.distroseries:
202+ self.logger.warning(
203+ "Encountered unexpected personal branch %s",
204+ db_branch.unique_name)
205+ return False
206+ find_branch_links = getUtility(IFindOfficialBranchLinks)
207+ links = list(find_branch_links.findForBranch(db_branch))
208+ if len(links) == 0:
209+ self.logger.warning(
210+ "%s is not an official branch", db_branch.unique_name)
211+ return False
212+ elif len(links) > 1:
213+ series_text = ', '.join([
214+ link.sourcepackage.path for link in links])
215+ self.logger.warning(
216+ "%s is official for multiple series: %s",
217+ db_branch.unique_name, series_text)
218+ return False
219+ elif links[0].sourcepackage != db_branch.sourcepackage:
220+ self.logger.warning(
221+ "%s is the official branch for %s but not its "
222+ "sourcepackage", db_branch.unique_name,
223+ links[0].sourcepackage.path)
224+ return False
225+ return True
226+
227+ def makeNewBranches(self):
228+ """Make official branches in the new distroseries."""
229+ for db_branch in self._existingOfficialBranches():
230+ self.logger.debug("Processing %s" % db_branch.unique_name)
231+ try:
232+ self.makeOneNewBranch(db_branch)
233+ except BranchExists:
234+ pass
235+
236+ def checkNewBranches(self):
237+ """Check the branches in the new distroseries are present and correct.
238+
239+ This function checks that every official package branch in the old
240+ distroseries has a matching branch in the new distroseries and that
241+ stacking is set up as we expect in both the hosted and mirrored areas
242+ on disk.
243+
244+ Every branch will be checked, even if some fail.
245+
246+ This function simply returns True or False -- any problems will be
247+ logged to ``self.logger``.
248+
249+ :return: ``True`` if every branch passes the check, ``False``
250+ otherwise.
251+ """
252+ ok = True
253+ for db_branch in self._existingOfficialBranches():
254+ self.logger.debug("Checking %s" % db_branch.unique_name)
255+ try:
256+ if not self.checkOneBranch(db_branch):
257+ ok = False
258+ except:
259+ ok = False
260+ self.logger.exception(
261+ "Unexpected error checking %s!", db_branch)
262+ return ok
263+
264+ def checkOneBranch(self, old_db_branch):
265+ """Check a branch in the old distroseries has been copied to the new.
266+
267+ This function checks that `old_db_branch` has a matching branch in the
268+ new distroseries and that stacking is set up as we expect in both the
269+ hosted and mirrored areas on disk.
270+
271+ This function simply returns True or False -- any problems will be
272+ logged to ``self.logger``.
273+
274+ :param old_db_branch: The branch to check.
275+ :return: ``True`` if the branch passes the check, ``False`` otherwise.
276+ """
277+ ok = self.checkConsistentOfficialPackageBranch(old_db_branch)
278+ if not ok:
279+ return ok
280+ new_sourcepackage = self.new_distroseries.getSourcePackage(
281+ old_db_branch.sourcepackagename)
282+ new_db_branch = new_sourcepackage.getBranch(
283+ PackagePublishingPocket.RELEASE)
284+ if new_db_branch is None:
285+ self.logger.warning(
286+ "No official branch found for %s",
287+ new_sourcepackage.path)
288+ return False
289+ ok = self.checkConsistentOfficialPackageBranch(new_db_branch)
290+ if not ok:
291+ return ok
292+ # for both mirrored and hosted areas:
293+ for scheme in 'lp-mirrored', 'lp-hosted':
294+ # the branch in the new distroseries is unstacked
295+ new_location = str(URI(
296+ scheme=scheme, host='', path='/' + new_db_branch.unique_name))
297+ try:
298+ new_bzr_branch = Branch.open(new_location)
299+ except NotBranchError:
300+ self.logger.warning(
301+ "No bzr branch at new location %s", new_location)
302+ ok = False
303+ else:
304+ try:
305+ new_stacked_on_url = new_bzr_branch.get_stacked_on_url()
306+ ok = False
307+ self.logger.warning(
308+ "New branch at %s is stacked on %s, should be "
309+ "unstacked.", new_location, new_stacked_on_url)
310+ except NotStacked:
311+ pass
312+ # The branch in the old distroseries is stacked on that in the
313+ # new.
314+ old_location = str(URI(
315+ scheme=scheme, host='', path='/' + old_db_branch.unique_name))
316+ try:
317+ old_bzr_branch = Branch.open(old_location)
318+ except NotBranchError:
319+ self.logger.warning(
320+ "No bzr branch at old location %s", old_location)
321+ ok = False
322+ else:
323+ try:
324+ old_stacked_on_url = old_bzr_branch.get_stacked_on_url()
325+ if old_stacked_on_url != '/' + new_db_branch.unique_name:
326+ self.logger.warning(
327+ "Old branch at %s is stacked on %s, should be "
328+ "stacked on %s", old_location, old_stacked_on_url,
329+ '/' + new_db_branch.unique_name)
330+ ok = False
331+ except NotStacked:
332+ self.logger.warning(
333+ "Old branch at %s is not stacked, should be stacked "
334+ "on %s", old_location,
335+ '/' + new_db_branch.unique_name)
336+ ok = False
337+ # The branch in the old distroseries has no revisions in its
338+ # repository. We open the repository independently of the
339+ # branch because the branch's repository has had its fallback
340+ # location activated. Note that this check might fail if new
341+ # revisions get pushed to the branch in the old distroseries,
342+ # which shouldn't happen but isn't totally impossible.
343+ old_repo = BzrDir.open(old_location).open_repository()
344+ if len(old_repo.all_revision_ids()) > 0:
345+ self.logger.warning(
346+ "Repository at %s has %s revisions.",
347+ old_location, len(old_repo.all_revision_ids()))
348+ ok = False
349+ # The branch in the old distroseries has at least some
350+ # history. (We can't check that the tips are the same because
351+ # the branch in the new distroseries might have new revisons).
352+ if old_bzr_branch.last_revision() == 'null:':
353+ self.logger.warning(
354+ "Old branch at %s has null tip revision.",
355+ old_location)
356+ ok = False
357+ return ok
358+
359+ def makeOneNewBranch(self, old_db_branch):
360+ """Copy a branch to the new distroseries.
361+
362+ This function makes a new database branch for the same source package
363+ as old_db_branch but in the new distroseries and then uses
364+ `switch_branches` to move the underlying bzr branch to the new series
365+ and replace the old branch with a branch stacked on the new series'
366+ branch.
367+
368+ :param old_db_branch: The branch to copy into the new distroseries.
369+ :raises BranchExists: This will be raised if old_db_branch has already
370+ been copied to the new distroseries (in the database, at least).
371+ """
372+ if not self.checkConsistentOfficialPackageBranch(old_db_branch):
373+ self.logger.warning("Skipping branch")
374+ return
375+ new_namespace = getUtility(IBranchNamespaceSet).get(
376+ person=old_db_branch.owner, product=None,
377+ distroseries=self.new_distroseries,
378+ sourcepackagename=old_db_branch.sourcepackagename)
379+ new_db_branch = new_namespace.createBranch(
380+ BranchType.HOSTED, old_db_branch.name, old_db_branch.registrant)
381+ new_db_branch.sourcepackage.setBranch(
382+ PackagePublishingPocket.RELEASE, new_db_branch,
383+ getUtility(ILaunchpadCelebrities).ubuntu_branches.teamowner)
384+ # switch_branches *moves* the data to locations dependent on the
385+ # new_branch's id, so if the transaction was rolled back we wouldn't
386+ # know the branch id and thus wouldn't be able to find the branch data
387+ # again. So commit before doing that.
388+ transaction.commit()
389+ switch_branches(
390+ config.codehosting.hosted_branches_root,
391+ 'lp-hosted', old_db_branch, new_db_branch)
392+ switch_branches(
393+ config.codehosting.mirrored_branches_root,
394+ 'lp-mirrored', old_db_branch, new_db_branch)
395+ return new_db_branch
396
397=== added file 'lib/lp/codehosting/tests/test_branchdistro.py'
398--- lib/lp/codehosting/tests/test_branchdistro.py 1970-01-01 00:00:00 +0000
399+++ lib/lp/codehosting/tests/test_branchdistro.py 2009-10-14 00:31:13 +0000
400@@ -0,0 +1,685 @@
401+# Copyright 2009 Canonical Ltd. This software is licensed under the
402+# GNU Affero General Public License version 3 (see the file LICENSE).
403+
404+"""Tests for making new source package branches just after a distro release.
405+"""
406+
407+__metaclass__ = type
408+
409+import os
410+import re
411+from StringIO import StringIO
412+from subprocess import PIPE, Popen, STDOUT
413+import textwrap
414+import unittest
415+
416+from bzrlib.branch import Branch
417+from bzrlib.bzrdir import BzrDir
418+from bzrlib.errors import NotStacked
419+from bzrlib.tests import TestCaseWithTransport
420+from bzrlib.transport import get_transport
421+from bzrlib.transport.chroot import ChrootServer
422+
423+from lazr.uri import URI
424+
425+import transaction
426+
427+from canonical.config import config
428+from canonical.testing.layers import ZopelessAppServerLayer
429+from canonical.launchpad.scripts.logger import FakeLogger, QuietFakeLogger
430+
431+from lp.codehosting.branchdistro import DistroBrancher, switch_branches
432+from lp.codehosting.vfs import branch_id_to_path
433+from lp.registry.interfaces.pocket import PackagePublishingPocket
434+from lp.testing import TestCaseWithFactory
435+
436+
437+# We say "RELEASE" often enough to not want to say "PackagePublishingPocket."
438+# each time.
439+RELEASE = PackagePublishingPocket.RELEASE
440+
441+
442+class FakeBranch:
443+ """Just enough of a Branch to pass `test_switch_branches`."""
444+
445+ def __init__(self, id):
446+ self.id = id
447+
448+ @property
449+ def unique_name(self):
450+ return branch_id_to_path(self.id)
451+
452+
453+class TestSwitchBranches(TestCaseWithTransport):
454+ """Tests for `switch_branches`."""
455+
456+ def test_switch_branches(self):
457+ # switch_branches moves a branch to the new location and places a
458+ # branch (with no revisions) stacked on the new branch in the old
459+ # location.
460+
461+ chroot_server = ChrootServer(self.get_transport())
462+ chroot_server.setUp()
463+ self.addCleanup(chroot_server.tearDown)
464+ scheme = chroot_server.get_url().rstrip('/:')
465+
466+ old_branch = FakeBranch(1)
467+ self.get_transport(old_branch.unique_name).create_prefix()
468+ tree = self.make_branch_and_tree(old_branch.unique_name)
469+ tree.commit(message='.')
470+
471+ new_branch = FakeBranch(2)
472+
473+ switch_branches('.', scheme, old_branch, new_branch)
474+
475+ # Post conditions:
476+ # 1. unstacked branch in new_branch's location
477+ # 2. stacked branch with no revisions in repo at old_branch
478+ # 3. last_revision() the same for two branches
479+
480+ old_location_bzrdir = BzrDir.open(str(URI(
481+ scheme=scheme, host='', path='/' + old_branch.unique_name)))
482+ new_location_bzrdir = BzrDir.open(str(URI(
483+ scheme=scheme, host='', path='/' + new_branch.unique_name)))
484+
485+ old_location_branch = old_location_bzrdir.open_branch()
486+ new_location_branch = new_location_bzrdir.open_branch()
487+
488+ # 1. unstacked branch in new_branch's location
489+ self.assertRaises(NotStacked, new_location_branch.get_stacked_on_url)
490+
491+ # 2. stacked branch with no revisions in repo at old_branch
492+ self.assertEqual(
493+ '/' + new_branch.unique_name,
494+ old_location_branch.get_stacked_on_url())
495+ self.assertEqual(
496+ [], old_location_bzrdir.open_repository().all_revision_ids())
497+
498+ # 3. last_revision() the same for two branches
499+ self.assertEqual(
500+ old_location_branch.last_revision(),
501+ new_location_branch.last_revision())
502+
503+
504+class TestDistroBrancher(TestCaseWithFactory):
505+ """Tests for `DistroBrancher`."""
506+
507+ layer = ZopelessAppServerLayer
508+
509+ def setUp(self):
510+ TestCaseWithFactory.setUp(self)
511+ self.useBzrBranches(real_server=True)
512+
513+ def makeOfficialPackageBranch(self, distroseries=None):
514+ """Make an official package branch with an underlying bzr branch."""
515+ db_branch = self.factory.makePackageBranch(distroseries=distroseries)
516+ db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner)
517+
518+ transaction.commit()
519+
520+ _, tree = self.create_branch_and_tree(
521+ tree_location=self.factory.getUniqueString(), db_branch=db_branch,
522+ hosted=True)
523+ tree.commit('')
524+ mirrored_branch = BzrDir.create_branch_convenience(
525+ db_branch.warehouse_url)
526+ mirrored_branch.pull(tree.branch)
527+
528+ return db_branch
529+
530+ def makeNewSeriesAndBrancher(self, distroseries=None):
531+ """Make a DistroBrancher.
532+
533+ Any messages logged by this DistroBrancher can be checked by calling
534+ `assertLogMessages` below.
535+ """
536+ if distroseries is None:
537+ distroseries = self.factory.makeDistroRelease()
538+ self._log_file = StringIO()
539+ new_distroseries = self.factory.makeDistroRelease(
540+ distribution=distroseries.distribution, name='new')
541+ transaction.commit()
542+ self.layer.switchDbUser('branch-distro')
543+ return DistroBrancher(
544+ FakeLogger(self._log_file), distroseries, new_distroseries)
545+
546+ def clearLogMessages(self):
547+ """Forget about all logged messages seen so far."""
548+ self._log_file.seek(0, 0)
549+ self._log_file.truncate()
550+
551+ def assertLogMessages(self, patterns):
552+ """Assert that the messages logged meet expectations.
553+
554+ :param patterns: A list of regular expressions. The length must match
555+ the number of messages logged, and then each pattern must match
556+ the messages logged in order.
557+ """
558+ log_messages = self._log_file.getvalue().splitlines()
559+ if len(log_messages) > len(patterns):
560+ self.fail(
561+ "More log messages (%s) than expected (%s)" %
562+ (log_messages, patterns))
563+ elif len(log_messages) < len(patterns):
564+ self.fail(
565+ "Fewer log messages (%s) than expected (%s)" %
566+ (log_messages, patterns))
567+ for pattern, message in zip(patterns, log_messages):
568+ if not re.match(pattern, message):
569+ self.fail("%r does not match %r" % (pattern, message))
570+
571+ def test_DistroBrancher_same_distro_check(self):
572+ # DistroBrancher.__init__ raises AssertionError if the two
573+ # distroseries passed are not from the same distribution.
574+ self.assertRaises(
575+ AssertionError, DistroBrancher, None,
576+ self.factory.makeDistroRelease(),
577+ self.factory.makeDistroRelease())
578+
579+ def test_DistroBrancher_same_distroseries_check(self):
580+ # DistroBrancher.__init__ raises AssertionError if passed the same
581+ # distroseries twice.
582+ distroseries = self.factory.makeDistroRelease()
583+ self.assertRaises(
584+ AssertionError, DistroBrancher, None, distroseries, distroseries)
585+
586+ def test_fromNames(self):
587+ # DistroBrancher.fromNames constructs a DistroBrancher from the names
588+ # of a distribution and two distroseries within it.
589+ distribution = self.factory.makeDistribution()
590+ distroseries1 = self.factory.makeDistroRelease(
591+ distribution=distribution)
592+ distroseries2 = self.factory.makeDistroRelease(
593+ distribution=distribution)
594+ brancher = DistroBrancher.fromNames(
595+ None, distribution.name, distroseries1.name, distroseries2.name)
596+ self.assertEqual(
597+ [distroseries1, distroseries2],
598+ [brancher.old_distroseries, brancher.new_distroseries])
599+
600+ # A word on testing strategy: we don't directly test the post conditions
601+ # of makeOneNewBranch, but we do test that it satisfies checkOneBranch and
602+ # the tests for checkOneBranch verify that this function rejects various
603+ # ways in which makeOneNewBranch could conceivably fail.
604+
605+ def test_makeOneNewBranch(self):
606+ # makeOneNewBranch creates an official package branch in the new
607+ # distroseries.
608+ db_branch = self.makeOfficialPackageBranch()
609+
610+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
611+ brancher.makeOneNewBranch(db_branch)
612+
613+ new_branch = brancher.new_distroseries.getSourcePackage(
614+ db_branch.sourcepackage.name).getBranch(RELEASE)
615+
616+ self.assertIsNot(None, new_branch)
617+ # The branch owner/name/target is the same, apart from the
618+ # distroseries.
619+ self.assertEqual(
620+ [db_branch.owner, db_branch.distribution,
621+ db_branch.sourcepackagename, db_branch.name],
622+ [new_branch.owner, new_branch.distribution,
623+ new_branch.sourcepackagename, new_branch.name])
624+
625+ def test_makeOneNewBranch_inconsistent_branch(self):
626+ # makeOneNewBranch skips over an inconsistent official package branch
627+ # (see `checkConsistentOfficialPackageBranch` for precisely what an
628+ # "inconsistent official package branch" is).
629+ unofficial_branch = self.factory.makePackageBranch()
630+ brancher = self.makeNewSeriesAndBrancher(
631+ unofficial_branch.distroseries)
632+ brancher.makeOneNewBranch(unofficial_branch)
633+
634+ new_branch = brancher.new_distroseries.getSourcePackage(
635+ unofficial_branch.sourcepackage.name).getBranch(RELEASE)
636+ self.assertIs(None, new_branch)
637+ self.assertLogMessages(
638+ ['^WARNING .* is not an official branch$',
639+ '^WARNING Skipping branch$'])
640+
641+ def test_makeNewBranches(self):
642+ # makeNewBranches calls makeOneNewBranch for each official branch in
643+ # the old distroseries.
644+ db_branch = self.makeOfficialPackageBranch()
645+ db_branch2 = self.makeOfficialPackageBranch(
646+ distroseries=db_branch.distroseries)
647+
648+ new_distroseries = self.factory.makeDistroRelease(
649+ distribution=db_branch.distribution)
650+
651+ brancher = DistroBrancher(
652+ QuietFakeLogger(), db_branch.distroseries, new_distroseries)
653+
654+ brancher.makeNewBranches()
655+
656+ new_sourcepackage = new_distroseries.getSourcePackage(
657+ db_branch.sourcepackage.name)
658+ new_branch = new_sourcepackage.getBranch(RELEASE)
659+ new_sourcepackage2 = new_distroseries.getSourcePackage(
660+ db_branch2.sourcepackage.name)
661+ new_branch2 = new_sourcepackage2.getBranch(RELEASE)
662+
663+ self.assertIsNot(None, new_branch)
664+ self.assertIsNot(None, new_branch2)
665+
666+ def test_makeNewBranches_idempotent(self):
667+ # makeNewBranches is idempotent in the sense that if a branch in the
668+ # old distroseries already has a counterpart in the new distroseries,
669+ # it is silently ignored.
670+ db_branch = self.makeOfficialPackageBranch()
671+
672+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
673+ brancher.makeNewBranches()
674+ brancher.makeNewBranches()
675+
676+ new_branch = brancher.new_distroseries.getSourcePackage(
677+ db_branch.sourcepackage.name).getBranch(RELEASE)
678+
679+ self.assertIsNot(new_branch, None)
680+
681+ def test_makeOneNewBranch_checks_ok(self):
682+ # After calling makeOneNewBranch for a branch, calling checkOneBranch
683+ # returns True for that branch.
684+ db_branch = self.makeOfficialPackageBranch()
685+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
686+ brancher.makeOneNewBranch(db_branch)
687+ self.clearLogMessages()
688+ ok = brancher.checkOneBranch(db_branch)
689+ self.assertLogMessages([])
690+ self.assertTrue(ok)
691+
692+ def test_checkConsistentOfficialPackageBranch_product_branch(self):
693+ # checkConsistentOfficialPackageBranch returns False when passed a
694+ # product branch.
695+ db_branch = self.factory.makeProductBranch()
696+ brancher = self.makeNewSeriesAndBrancher()
697+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
698+ self.assertLogMessages([
699+ '^WARNING Encountered unexpected product branch .*/.*/.*$'])
700+ self.assertFalse(ok)
701+
702+ def test_checkConsistentOfficialPackageBranch_personal_branch(self):
703+ # checkConsistentOfficialPackageBranch returns False when passed a
704+ # personal branch.
705+ db_branch = self.factory.makePersonalBranch()
706+ brancher = self.makeNewSeriesAndBrancher()
707+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
708+ self.assertLogMessages([
709+ '^WARNING Encountered unexpected personal branch .*/.*/.*$'])
710+ self.assertFalse(ok)
711+
712+ def test_checkConsistentOfficialPackageBranch_no_official_branch(self):
713+ # checkConsistentOfficialPackageBranch returns False when passed a
714+ # branch which is not official for any package.
715+ db_branch = self.factory.makePackageBranch()
716+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
717+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
718+ self.assertLogMessages(
719+ ['^WARNING .*/.*/.* is not an official branch$'])
720+ self.assertFalse(ok)
721+
722+ def test_checkConsistentOfficialPackageBranch_official_elsewhere(self):
723+ # checkConsistentOfficialPackageBranch returns False when passed a
724+ # branch which is official for a sourcepackage that it is not a branch
725+ # for.
726+ db_branch = self.factory.makePackageBranch()
727+ self.factory.makeSourcePackage().setBranch(
728+ RELEASE, db_branch, db_branch.owner)
729+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
730+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
731+ self.assertLogMessages(
732+ ['^WARNING .*/.*/.* is the official branch for .*/.*/.* but not '
733+ 'its sourcepackage$'])
734+ self.assertFalse(ok)
735+
736+ def test_checkConsistentOfficialPackageBranch_official_twice(self):
737+ # checkConsistentOfficialPackageBranch returns False when passed a
738+ # branch that is official for two sourcepackages.
739+ db_branch = self.factory.makePackageBranch()
740+ db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner)
741+ self.factory.makeSourcePackage().setBranch(
742+ RELEASE, db_branch, db_branch.owner)
743+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
744+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
745+ self.assertLogMessages([
746+ '^WARNING .*/.*/.* is official for multiple series: .*/.*/.*, '
747+ '.*/.*/.*$'])
748+ self.assertFalse(ok)
749+
750+ def test_checkConsistentOfficialPackageBranch_ok(self):
751+ # checkConsistentOfficialPackageBranch returns True when passed a
752+ # branch that is official for its sourcepackage and no other.
753+ db_branch = self.factory.makePackageBranch()
754+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
755+ db_branch.sourcepackage.setBranch(RELEASE, db_branch, db_branch.owner)
756+ ok = brancher.checkConsistentOfficialPackageBranch(db_branch)
757+ self.assertLogMessages([])
758+ self.assertTrue(ok)
759+
760+ def test_checkOneBranch_inconsistent_old_package_branch(self):
761+ # checkOneBranch returns False when passed a branch that is not a
762+ # consistent official package branch.
763+ db_branch = self.factory.makePackageBranch()
764+ brancher = self.makeNewSeriesAndBrancher()
765+ ok = brancher.checkOneBranch(db_branch)
766+ self.assertFalse(ok)
767+ self.assertLogMessages(
768+ ['^WARNING .*/.*/.* is not an official branch$'])
769+
770+ def test_checkOneBranch_no_new_official_branch(self):
771+ # checkOneBranch returns False when there is no corresponding official
772+ # package branch in the new distroseries.
773+ db_branch = self.makeOfficialPackageBranch()
774+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
775+ ok = brancher.checkOneBranch(db_branch)
776+ self.assertFalse(ok)
777+ self.assertLogMessages(
778+ ['^WARNING No official branch found for .*/.*/.*$'])
779+
780+ def test_checkOneBranch_inconsistent_new_package_branch(self):
781+ # checkOneBranch returns False when the corresponding official package
782+ # branch in the new distroseries is not consistent.
783+ db_branch = self.makeOfficialPackageBranch()
784+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
785+ new_db_branch = brancher.makeOneNewBranch(db_branch)
786+ self.layer.switchDbUser('launchpad')
787+ new_db_branch.setTarget(
788+ new_db_branch.owner,
789+ source_package=self.factory.makeSourcePackage())
790+ transaction.commit()
791+ self.layer.switchDbUser('branch-distro')
792+ ok = brancher.checkOneBranch(new_db_branch)
793+ self.assertFalse(ok)
794+ self.assertLogMessages(
795+ ['^WARNING .*/.*/.* is the official branch for .*/.*/.* but not '
796+ 'its sourcepackage$'])
797+
798+ def checkOneBranch_new_branch_missing(self, branch_type):
799+ # checkOneBranch returns False when there is no bzr branch for the
800+ # database branch in the new distroseries.
801+ assert branch_type in ('hosted', 'mirrored')
802+ db_branch = self.makeOfficialPackageBranch()
803+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
804+ new_db_branch = brancher.makeOneNewBranch(db_branch)
805+ if branch_type == 'hosted':
806+ url = new_db_branch.getPullURL()
807+ else:
808+ url = new_db_branch.warehouse_url
809+ get_transport(url).delete_tree('.bzr')
810+ ok = brancher.checkOneBranch(db_branch)
811+ self.assertFalse(ok)
812+ # Deleting the new branch will break the old branch, as that's stacked
813+ # on the new one.
814+ self.assertLogMessages([
815+ '^WARNING No bzr branch at new location lp-%s:///.*/.*/.*/.*$'
816+ % branch_type,
817+ '^WARNING No bzr branch at old location lp-%s:///.*/.*/.*/.*$'
818+ % branch_type,
819+ ])
820+
821+ def test_checkOneBranch_new_hosted_branch_missing(self):
822+ # checkOneBranch returns False when there is no bzr branch in the
823+ # hosted area for the database branch in the new distroseries.
824+ self.checkOneBranch_new_branch_missing('hosted')
825+
826+ def test_checkOneBranch_new_mirrored_branch_missing(self):
827+ # checkOneBranch returns False when there is no bzr branch in the
828+ # mirrored area for the database branch in the new distroseries.
829+ self.checkOneBranch_new_branch_missing('mirrored')
830+
831+ def checkOneBranch_old_branch_missing(self, branch_type):
832+ # checkOneBranch returns False when there is no bzr branchfor the
833+ # database branch in old distroseries.
834+ assert branch_type in ('hosted', 'mirrored')
835+ db_branch = self.makeOfficialPackageBranch()
836+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
837+ brancher.makeOneNewBranch(db_branch)
838+ if branch_type == 'hosted':
839+ url = db_branch.getPullURL()
840+ else:
841+ url = db_branch.warehouse_url
842+ get_transport(url).delete_tree('.bzr')
843+ ok = brancher.checkOneBranch(db_branch)
844+ self.assertFalse(ok)
845+ self.assertLogMessages([
846+ '^WARNING No bzr branch at old location lp-%s:///.*/.*/.*/.*$'
847+ % branch_type,
848+ ])
849+
850+ def test_checkOneBranch_old_hosted_branch_missing(self):
851+ # checkOneBranch returns False when there is no bzr branch in the
852+ # hosted area for the database branch in old distroseries.
853+ self.checkOneBranch_old_branch_missing('hosted')
854+
855+ def test_checkOneBranch_old_mirrored_branch_missing(self):
856+ # checkOneBranch returns False when there is no bzr branch in the
857+ # mirrored area for the database branch in old distroseries.
858+ self.checkOneBranch_old_branch_missing('mirrored')
859+
860+ def checkOneBranch_new_stacked(self, branch_type):
861+ # checkOneBranch returns False when the bzr branch for the database
862+ # branch in new distroseries is stacked.
863+ assert branch_type in ('hosted', 'mirrored')
864+ db_branch = self.makeOfficialPackageBranch()
865+ b, _ = self.create_branch_and_tree(
866+ self.factory.getUniqueString(), hosted=(branch_type == 'hosted'))
867+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
868+ new_db_branch = brancher.makeOneNewBranch(db_branch)
869+ if branch_type == 'hosted':
870+ url = new_db_branch.getPullURL()
871+ else:
872+ url = new_db_branch.warehouse_url
873+ Branch.open(url).set_stacked_on_url('/' + b.unique_name)
874+ ok = brancher.checkOneBranch(db_branch)
875+ self.assertFalse(ok)
876+ self.assertLogMessages([
877+ '^WARNING New branch at lp-%s:///.*/.*/.*/.* is stacked on '
878+ '/.*/.*/.*, should be unstacked.$' % branch_type,
879+ ])
880+
881+ def test_checkOneBranch_new_hosted_stacked(self):
882+ # checkOneBranch returns False when the bzr branch in the hosted area
883+ # for the database branch in new distroseries is stacked.
884+ self.checkOneBranch_new_stacked('hosted')
885+
886+ def test_checkOneBranch_new_mirrored_stacked(self):
887+ # checkOneBranch returns False when the bzr branch in the mirrored
888+ # area for the database branch in new distroseries is stacked.
889+ self.checkOneBranch_new_stacked('mirrored')
890+
891+ def checkOneBranch_old_unstacked(self, branch_type):
892+ # checkOneBranch returns False when the bzr branch for the database
893+ # branch in old distroseries is not stacked.
894+ assert branch_type in ('hosted', 'mirrored')
895+ db_branch = self.makeOfficialPackageBranch()
896+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
897+ brancher.makeOneNewBranch(db_branch)
898+ if branch_type == 'hosted':
899+ url = db_branch.getPullURL()
900+ else:
901+ url = db_branch.warehouse_url
902+ old_bzr_branch = Branch.open(url)
903+ old_bzr_branch.set_stacked_on_url(None)
904+ ok = brancher.checkOneBranch(db_branch)
905+ self.assertLogMessages([
906+ '^WARNING Old branch at lp-%s:///.*/.*/.*/.* is not stacked, '
907+ 'should be stacked on /.*/.*/.*.$' % branch_type,
908+ '^.*has .* revisions.*$',
909+ ])
910+ self.assertFalse(ok)
911+
912+ def test_checkOneBranch_old_hosted_unstacked(self):
913+ # checkOneBranch returns False when the bzr branch in the hosted area
914+ # for the database branch in old distroseries is not stacked.
915+ self.checkOneBranch_old_unstacked('hosted')
916+
917+ def test_checkOneBranch_old_mirrored_unstacked(self):
918+ # checkOneBranch returns False when the bzr branch in the mirrored
919+ # area for the database branch in old distroseries is not stacked.
920+ self.checkOneBranch_old_unstacked('mirrored')
921+
922+ def checkOneBranch_old_misstacked(self, branch_type):
923+ # checkOneBranch returns False when the bzr branch for the database
924+ # branch in old distroseries stacked on some other branch than the
925+ # branch in the new distroseries.
926+ assert branch_type in ('hosted', 'mirrored')
927+ db_branch = self.makeOfficialPackageBranch()
928+ b, _ = self.create_branch_and_tree(
929+ self.factory.getUniqueString(), hosted=(branch_type == 'hosted'))
930+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
931+ brancher.makeOneNewBranch(db_branch)
932+ if branch_type == 'hosted':
933+ url = db_branch.getPullURL()
934+ else:
935+ url = db_branch.warehouse_url
936+ Branch.open(url).set_stacked_on_url('/' + b.unique_name)
937+ ok = brancher.checkOneBranch(db_branch)
938+ self.assertLogMessages([
939+ '^WARNING Old branch at lp-%s:///.*/.*/.*/.* is stacked on '
940+ '/.*/.*/.*, should be stacked on /.*/.*/.*.$' % branch_type,
941+ ])
942+ self.assertFalse(ok)
943+
944+ def test_checkOneBranch_old_hosted_misstacked(self):
945+ # checkOneBranch returns False when the bzr branch in the hosted area
946+ # for the database branch in old distroseries stacked on some other
947+ # branch than the branch in the new distroseries.
948+ self.checkOneBranch_old_misstacked('hosted')
949+
950+ def test_checkOneBranch_old_mirrored_misstacked(self):
951+ # checkOneBranch returns False when the bzr branch in the mirrored
952+ # area for the database branch in old distroseries stacked on some
953+ # other branch than the branch in the new distroseries.
954+ self.checkOneBranch_old_misstacked('mirrored')
955+
956+ def checkOneBranch_old_has_revisions(self, branch_type):
957+ # checkOneBranch returns False when the bzr branch for the database
958+ # branch in old distroseries has a repository that contains revisions.
959+ assert branch_type in ('hosted', 'mirrored')
960+ db_branch = self.makeOfficialPackageBranch()
961+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
962+ brancher.makeOneNewBranch(db_branch)
963+ if branch_type == 'hosted':
964+ url = db_branch.getPullURL()
965+ else:
966+ url = db_branch.warehouse_url
967+ old_bzr_branch = Branch.open(url)
968+ old_bzr_branch.create_checkout(
969+ self.factory.getUniqueString()).commit('')
970+ ok = brancher.checkOneBranch(db_branch)
971+ self.assertLogMessages([
972+ '^WARNING Repository at lp-%s:///.*/.*/.*/.* has 1 revisions.'
973+ % branch_type
974+ ])
975+ self.assertFalse(ok)
976+
977+ def test_checkOneBranch_old_hosted_has_revisions(self):
978+ # checkOneBranch returns False when the bzr branch in the hosted area
979+ # for the database branch in old distroseries has a repository that
980+ # contains revisions.
981+ self.checkOneBranch_old_has_revisions('hosted')
982+
983+ def test_checkOneBranch_old_mirrored_has_revisions(self):
984+ # checkOneBranch returns False when the bzr branch in the mirrored
985+ # area for the database branch in old distroseries has a repository
986+ # that contains revisions.
987+ self.checkOneBranch_old_has_revisions('mirrored')
988+
989+ def checkOneBranch_old_has_null_tip(self, branch_type):
990+ # checkOneBranch returns False when the bzr branch for the database
991+ # branch in old distroseries has tip revision of 'null:'.
992+ assert branch_type in ('hosted', 'mirrored')
993+ db_branch = self.makeOfficialPackageBranch()
994+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
995+ brancher.makeOneNewBranch(db_branch)
996+ if branch_type == 'hosted':
997+ url = db_branch.getPullURL()
998+ else:
999+ url = db_branch.warehouse_url
1000+ old_bzr_branch = Branch.open(url)
1001+ old_bzr_branch.set_last_revision_info(0, 'null:')
1002+ ok = brancher.checkOneBranch(db_branch)
1003+ self.assertLogMessages([
1004+ '^WARNING Old branch at lp-%s:///.*/.*/.*/.* has null tip '
1005+ 'revision.' % branch_type
1006+ ])
1007+ self.assertFalse(ok)
1008+
1009+ def test_checkOneBranch_old_hosted_has_null_tip(self):
1010+ # checkOneBranch returns False when the bzr branch in the hosted area
1011+ # for the database branch in old distroseries has tip revision of
1012+ # 'null:'.
1013+ self.checkOneBranch_old_has_null_tip('hosted')
1014+
1015+ def test_checkOneBranch_old_mirrored_has_null_tip(self):
1016+ # checkOneBranch returns False when the bzr branch in the mirrored
1017+ # area for the database branch in old distroseries has tip revision of
1018+ # 'null:'.
1019+ self.checkOneBranch_old_has_null_tip('mirrored')
1020+
1021+ def runBranchDistroScript(self, args):
1022+ """Run the branch-distro.py script with the given arguments.
1023+
1024+ ;param args: The arguments to pass to the branch-distro.py script.
1025+ :return: A tuple (returncode, output). stderr and stdout are both
1026+ contained in the output.
1027+ """
1028+ script_path = os.path.join(config.root, 'scripts', 'branch-distro.py')
1029+ process = Popen([script_path] + args, stdout=PIPE, stderr=STDOUT)
1030+ output, error = process.communicate()
1031+ return process.returncode, output
1032+
1033+ def test_makeNewBranches_script(self):
1034+ # Running the script with the arguments 'distro old-series new-series'
1035+ # makes new branches in the new series.
1036+ db_branch = self.makeOfficialPackageBranch()
1037+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
1038+ returncode, output = self.runBranchDistroScript(
1039+ ['-v', db_branch.distribution.name,
1040+ brancher.old_distroseries.name, brancher.new_distroseries.name])
1041+ self.assertEqual(0, returncode)
1042+ self.assertEqual(
1043+ 'DEBUG Processing ' + db_branch.unique_name + '\n', output)
1044+ brancher.checkOneBranch(db_branch)
1045+
1046+ def test_checkNewBranches_script_success(self):
1047+ # Running the script with the arguments '--check distro old-series
1048+ # new-series' checks that the branches in the new series are as
1049+ # expected.
1050+ db_branch = self.makeOfficialPackageBranch()
1051+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
1052+ brancher.makeNewBranches()
1053+ returncode, output = self.runBranchDistroScript(
1054+ ['-v', '--check', db_branch.distribution.name,
1055+ brancher.old_distroseries.name, brancher.new_distroseries.name])
1056+ self.assertEqual(0, returncode)
1057+ self.assertEqual(
1058+ 'DEBUG Checking ' + db_branch.unique_name + '\n', output)
1059+ brancher.checkOneBranch(db_branch)
1060+
1061+ def test_checkNewBranches_script_failure(self):
1062+ # Running the script with the arguments '--check distro old-series
1063+ # new-series' checks that the branches in the new series are as
1064+ # expected and logs warnings and exits with code 1 is things are not
1065+ # as expected.
1066+ db_branch = self.makeOfficialPackageBranch()
1067+ brancher = self.makeNewSeriesAndBrancher(db_branch.distroseries)
1068+ returncode, output = self.runBranchDistroScript(
1069+ ['-v', '--check', db_branch.distribution.name,
1070+ brancher.old_distroseries.name, brancher.new_distroseries.name])
1071+ sp_path = brancher.new_distroseries.getSourcePackage(
1072+ db_branch.sourcepackagename).path
1073+ expected = '''\
1074+ DEBUG Checking %(branch_name)s
1075+ WARNING No official branch found for %(sp_path)s
1076+ ERROR Check failed
1077+ ''' % {'branch_name': db_branch.unique_name, 'sp_path': sp_path}
1078+ self.assertEqual(
1079+ textwrap.dedent(expected), output)
1080+ self.assertEqual(1, returncode)
1081+
1082+
1083+def test_suite():
1084+ return unittest.TestLoader().loadTestsFromName(__name__)
1085+
1086
1087=== added file 'scripts/branch-distro.py'
1088--- scripts/branch-distro.py 1970-01-01 00:00:00 +0000
1089+++ scripts/branch-distro.py 2009-10-14 00:31:13 +0000
1090@@ -0,0 +1,39 @@
1091+#!/usr/bin/python2.4
1092+#
1093+# Copyright 2009 Canonical Ltd. This software is licensed under the
1094+# GNU Affero General Public License version 3 (see the file LICENSE).
1095+
1096+import _pythonpath
1097+
1098+from lp.codehosting.branchdistro import DistroBrancher
1099+from lp.codehosting.vfs import get_multi_server
1100+from lp.services.scripts.base import LaunchpadScript, LaunchpadScriptFailure
1101+
1102+
1103+class BranchDistroScript(LaunchpadScript):
1104+
1105+ usage = "%prog distro old-series new-series"
1106+
1107+ def add_my_options(self):
1108+ self.parser.add_option(
1109+ '--check', dest="check", action="store_true", default=False,
1110+ help=("Check that the new distro series has its official "
1111+ "branches set up correctly."))
1112+
1113+ def main(self):
1114+ if len(self.args) != 3:
1115+ self.parser.error("Wrong number of arguments.")
1116+ brancher = DistroBrancher.fromNames(self.logger, *self.args)
1117+ server = get_multi_server(write_mirrored=True, write_hosted=True)
1118+ server.setUp()
1119+ try:
1120+ if self.options.check:
1121+ if not brancher.checkNewBranches():
1122+ raise LaunchpadScriptFailure("Check failed")
1123+ else:
1124+ brancher.makeNewBranches()
1125+ finally:
1126+ server.tearDown()
1127+
1128+if __name__ == '__main__':
1129+ BranchDistroScript("branch-distro", dbuser='branch-distro').run()