Merge lp:~cjwatson/launchpad/sourcedeps-codetree into lp:launchpad

Proposed by Colin Watson on 2017-11-01
Status: Needs review
Proposed branch: lp:~cjwatson/launchpad/sourcedeps-codetree
Merge into: lp:launchpad
Diff against target: 847 lines (+165/-553)
4 files modified
lib/devscripts/sourcecode.py (+49/-307)
lib/devscripts/tests/test_sourcecode.py (+116/-173)
utilities/sourcedeps.cache (+0/-70)
utilities/sourcedeps.filter (+0/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/sourcedeps-codetree
Reviewer Review Type Date Requested Status
Launchpad code reviewers 2017-11-01 Pending
Review via email: mp+333073@code.launchpad.net

Commit message

Replace most of devscripts.sourcecode with codetree.

Description of the change

This drops some bespoke code and potentially lets us migrate individual sourcedeps to git.

We'll need to install python-codetree in various places first (this is too early for us to be able to rely on a Python dependency rather than a system dependency).

To post a comment you must log in.

Unmerged revisions

18498. By Colin Watson on 2017-11-01

Replace most of devscripts.sourcecode with codetree.

18497. By Colin Watson on 2017-10-31

Drop support for private/optional sourcedeps, unused since r12871.

18496. By Colin Watson on 2017-10-31

Remove utilities/sourcedeps.filter, unused since r8510.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/devscripts/sourcecode.py'
--- lib/devscripts/sourcecode.py 2012-06-25 12:21:10 +0000
+++ lib/devscripts/sourcecode.py 2017-11-01 09:54:39 +0000
@@ -1,136 +1,37 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tools for maintaining the Launchpad source code."""4"""Tools for maintaining the Launchpad source code."""
55
6from __future__ import absolute_import, print_function, unicode_literals
7
6__metaclass__ = type8__metaclass__ = type
7__all__ = [9__all__ = [
8 'interpret_config',10 'main',
9 'parse_config_file',
10 'plan_update',
11 ]11 ]
1212
13import errno13import logging
14import json
15import optparse14import optparse
16import os15import os
17import shutil16import shutil
18import sys
1917
20from bzrlib import ui18from codetree.config import Config
21from bzrlib.branch import Branch19from codetree.handlers.bzr import BzrSourceHandler
22from bzrlib.errors import (
23 BzrError,
24 IncompatibleRepositories,
25 NotBranchError,
26 )
27from bzrlib.plugin import load_plugins
28from bzrlib.revisionspec import RevisionSpec
29from bzrlib.trace import (
30 enable_default_logging,
31 report_exception,
32 )
33from bzrlib.upgrade import upgrade
34from bzrlib.workingtree import WorkingTree
3520
36from devscripts import get_launchpad_root21from devscripts import get_launchpad_root
3722
3823
39def parse_config_file(file_handle):24def plan_update(existing_branches, config):
40 """Parse the source code config file 'file_handle'.
41
42 :param file_handle: A file-like object containing sourcecode
43 configuration.
44 :return: A sequence of lines of either '[key, value]' or
45 '[key, value, optional]'.
46 """
47 for line in file_handle:
48 if line == '\n' or line.startswith('#'):
49 continue
50 yield line.split()
51
52
53def interpret_config_entry(entry, use_http=False):
54 """Interpret a single parsed line from the config file."""
55 branch_name = entry[0]
56 components = entry[1].split(';revno=')
57 branch_url = components[0]
58 if use_http:
59 branch_url = branch_url.replace('lp:', 'http://bazaar.launchpad.net/')
60 if len(components) == 1:
61 revision = None
62 else:
63 assert len(components) == 2, 'Bad branch URL: ' + entry[1]
64 revision = components[1] or None
65 if len(entry) > 2:
66 assert len(entry) == 3 and entry[2].lower() == 'optional', (
67 'Bad configuration line: should be space delimited values of '
68 'sourcecode directory name, branch URL [, "optional"]\n' +
69 ' '.join(entry))
70 optional = True
71 else:
72 optional = False
73 return branch_name, branch_url, revision, optional
74
75
76def load_cache(cache_filename):
77 try:
78 cache_file = open(cache_filename, 'rb')
79 except IOError as e:
80 if e.errno == errno.ENOENT:
81 return {}
82 else:
83 raise
84 with cache_file:
85 return json.load(cache_file)
86
87
88def interpret_config(config_entries, public_only, use_http=False):
89 """Interpret a configuration stream, as parsed by 'parse_config_file'.
90
91 :param configuration: A sequence of parsed configuration entries.
92 :param public_only: If true, ignore private/optional branches.
93 :param use_http: If True, force all branch URLs to use http://
94 :return: A dict mapping the names of the sourcecode dependencies to a
95 2-tuple of their branches and whether or not they are optional.
96 """
97 config = {}
98 for entry in config_entries:
99 branch_name, branch_url, revision, optional = interpret_config_entry(
100 entry, use_http)
101 if not optional or not public_only:
102 config[branch_name] = (branch_url, revision, optional)
103 return config
104
105
106def _subset_dict(d, keys):
107 """Return a dict that's a subset of 'd', based on the keys in 'keys'."""
108 return dict((key, d[key]) for key in keys)
109
110
111def plan_update(existing_branches, configuration):
112 """Plan the update to existing branches based on 'configuration'.25 """Plan the update to existing branches based on 'configuration'.
11326
114 :param existing_branches: A sequence of branches that already exist.27 :param existing_branches: A sequence of branches that already exist.
115 :param configuration: A dictionary of sourcecode configuration, such as is28 :param config: An instance of `codetree.Config`.
116 returned by `interpret_config`.29 :return: a set of the branches that exist locally but not in the
117 :return: (new_branches, update_branches, removed_branches), where30 configuration.
118 'new_branches' are the branches in the configuration that don't exist
119 yet, 'update_branches' are the branches in the configuration that do
120 exist, and 'removed_branches' are the branches that exist locally, but
121 not in the configuration. 'new_branches' and 'update_branches' are
122 dicts of the same form as 'configuration', 'removed_branches' is a
123 set of the same form as 'existing_branches'.
124 """31 """
125 existing_branches = set(existing_branches)32 existing_branches = set(existing_branches)
126 config_branches = set(configuration.keys())33 config_branches = set(config.directive_map)
127 new_branches = config_branches - existing_branches34 return existing_branches - config_branches
128 removed_branches = existing_branches - config_branches
129 update_branches = config_branches.intersection(existing_branches)
130 return (
131 _subset_dict(configuration, new_branches),
132 _subset_dict(configuration, update_branches),
133 removed_branches)
13435
13536
136def find_branches(directory):37def find_branches(directory):
@@ -139,200 +40,50 @@
139 for name in os.listdir(directory):40 for name in os.listdir(directory):
140 if name in ('.', '..'):41 if name in ('.', '..'):
141 continue42 continue
142 try:43 for subdir in ('.bzr', '.git'):
143 Branch.open(os.path.join(directory, name))44 if os.path.exists(os.path.join(directory, name, subdir)):
144 branches.append(name)45 branches.append(name)
145 except NotBranchError:
146 pass
147 return branches46 return branches
14847
14948
150def get_revision_id(revision, from_branch, tip=False):49def remove_branches(sourcecode_directory, removed_branches):
151 """Return revision id for a revision number and a branch.
152
153 If the revision is empty, the revision_id will be None.
154
155 If ``tip`` is True, the revision value will be ignored.
156 """
157 if not tip and revision:
158 spec = RevisionSpec.from_string(revision)
159 return spec.as_revision_id(from_branch)
160 # else return None
161
162
163def _format_revision_name(revision, tip=False):
164 """Formatting helper to return human-readable identifier for revision.
165
166 If ``tip`` is True, the revision value will be ignored.
167 """
168 if not tip and revision:
169 return 'revision %s' % (revision,)
170 else:
171 return 'tip'
172
173
174def get_branches(sourcecode_directory, new_branches,
175 possible_transports=None, tip=False, quiet=False):
176 """Get the new branches into sourcecode."""
177 for project, (branch_url, revision, optional) in new_branches.iteritems():
178 destination = os.path.join(sourcecode_directory, project)
179 try:
180 remote_branch = Branch.open(
181 branch_url, possible_transports=possible_transports)
182 except BzrError:
183 if optional:
184 report_exception(sys.exc_info(), sys.stderr)
185 continue
186 else:
187 raise
188 possible_transports.append(
189 remote_branch.bzrdir.root_transport)
190 if not quiet:
191 print 'Getting %s from %s at %s' % (
192 project, branch_url, _format_revision_name(revision, tip))
193 # If the 'optional' flag is set, then it's a branch that shares
194 # history with Launchpad, so we should share repositories. Otherwise,
195 # we should avoid sharing repositories to avoid format
196 # incompatibilities.
197 force_new_repo = not optional
198 revision_id = get_revision_id(revision, remote_branch, tip)
199 remote_branch.bzrdir.sprout(
200 destination, revision_id=revision_id, create_tree_if_local=True,
201 source_branch=remote_branch, force_new_repo=force_new_repo,
202 possible_transports=possible_transports)
203
204
205def find_stale(updated, cache, sourcecode_directory, quiet):
206 """Find branches whose revision info doesn't match the cache."""
207 new_updated = dict(updated)
208 for project, (branch_url, revision, optional) in updated.iteritems():
209 cache_revision_info = cache.get(project)
210 if cache_revision_info is None:
211 continue
212 if cache_revision_info[0] != int(revision):
213 continue
214 destination = os.path.join(sourcecode_directory, project)
215 try:
216 branch = Branch.open(destination)
217 except BzrError:
218 continue
219 if list(branch.last_revision_info()) != cache_revision_info:
220 continue
221 if not quiet:
222 print '%s is already up to date.' % project
223 del new_updated[project]
224 return new_updated
225
226
227def update_cache(cache, cache_filename, changed, sourcecode_directory, quiet):
228 """Update the cache with the changed branches."""
229 old_cache = dict(cache)
230 for project, (branch_url, revision, optional) in changed.iteritems():
231 destination = os.path.join(sourcecode_directory, project)
232 branch = Branch.open(destination)
233 cache[project] = list(branch.last_revision_info())
234 if cache == old_cache:
235 return
236 with open(cache_filename, 'wb') as cache_file:
237 json.dump(cache, cache_file, indent=4, sort_keys=True)
238 if not quiet:
239 print 'Cache updated. Please commit "%s".' % cache_filename
240
241
242def update_branches(sourcecode_directory, update_branches,
243 possible_transports=None, tip=False, quiet=False):
244 """Update the existing branches in sourcecode."""
245 if possible_transports is None:
246 possible_transports = []
247 # XXX: JonathanLange 2009-11-09: Rather than updating one branch after
248 # another, we could instead try to get them in parallel.
249 for project, (branch_url, revision, optional) in (
250 update_branches.iteritems()):
251 # Update project from branch_url.
252 destination = os.path.join(sourcecode_directory, project)
253 if not quiet:
254 print 'Updating %s to %s' % (
255 project, _format_revision_name(revision, tip))
256 local_tree = WorkingTree.open(destination)
257 try:
258 remote_branch = Branch.open(
259 branch_url, possible_transports=possible_transports)
260 except BzrError:
261 if optional:
262 report_exception(sys.exc_info(), sys.stderr)
263 continue
264 else:
265 raise
266 possible_transports.append(
267 remote_branch.bzrdir.root_transport)
268 revision_id = get_revision_id(revision, remote_branch, tip)
269 try:
270 result = local_tree.pull(
271 remote_branch, stop_revision=revision_id, overwrite=True,
272 possible_transports=possible_transports)
273 except IncompatibleRepositories:
274 # XXX JRV 20100407: Ideally remote_branch.bzrdir._format
275 # should be passed into upgrade() to ensure the format is the same
276 # locally and remotely. Unfortunately smart server branches
277 # have their _format set to RemoteFormat rather than an actual
278 # format instance.
279 upgrade(destination)
280 # Upgraded, repoen working tree
281 local_tree = WorkingTree.open(destination)
282 result = local_tree.pull(
283 remote_branch, stop_revision=revision_id, overwrite=True,
284 possible_transports=possible_transports)
285 if result.old_revid == result.new_revid:
286 if not quiet:
287 print ' (No change)'
288 else:
289 if result.old_revno < result.new_revno:
290 change = 'Updated'
291 else:
292 change = 'Reverted'
293 if not quiet:
294 print ' (%s from %s to %s)' % (
295 change, result.old_revno, result.new_revno)
296
297
298def remove_branches(sourcecode_directory, removed_branches, quiet=False):
299 """Remove sourcecode that's no longer there."""50 """Remove sourcecode that's no longer there."""
300 for project in removed_branches:51 for project in removed_branches:
301 destination = os.path.join(sourcecode_directory, project)52 destination = os.path.join(sourcecode_directory, project)
302 if not quiet:53 logging.info('Removing %s', project)
303 print 'Removing %s' % project
304 try:54 try:
305 shutil.rmtree(destination)55 shutil.rmtree(destination)
306 except OSError:56 except OSError:
307 os.unlink(destination)57 os.unlink(destination)
30858
30959
310def update_sourcecode(sourcecode_directory, config_filename, cache_filename,60def mangle_config(sourcecode_directory, config, tip=False, use_http=False):
311 public_only, tip, dry_run, quiet=False, use_http=False):61 for directive in config.directive_map.values():
312 """Update the sourcecode."""62 directive.location = os.path.join(
313 config_file = open(config_filename)63 sourcecode_directory, directive.location)
314 config = interpret_config(64 if tip:
315 parse_config_file(config_file), public_only, use_http)65 directive.source_options.pop('revno', None)
316 config_file.close()66 if use_http:
317 cache = load_cache(cache_filename)67 handler = directive.source
68 if (isinstance(handler, BzrSourceHandler) and
69 handler.source.startswith('lp:')):
70 handler.source = handler.source.replace(
71 'lp:', 'http://bazaar.launchpad.net/')
72
73
74def update_sourcecode(sourcecode_directory, config_filename,
75 tip=False, dry_run=False, use_http=False):
76 config = Config([config_filename])
77 mangle_config(sourcecode_directory, config, tip=tip, use_http=use_http)
318 branches = find_branches(sourcecode_directory)78 branches = find_branches(sourcecode_directory)
319 new, updated, removed = plan_update(branches, config)79 removed = plan_update(branches, config)
320 possible_transports = []80 # XXX cjwatson 2017-10-31: If we start pulling sourcedeps from git, then
81 # we need to remove old bzr branches first.
82 config.build(dry_run=dry_run)
321 if dry_run:83 if dry_run:
322 print 'Branches to fetch:', new.keys()84 logging.info('Branches to remove: %s', list(removed))
323 print 'Branches to update:', updated.keys()
324 print 'Branches to remove:', list(removed)
325 else:85 else:
326 get_branches(86 remove_branches(sourcecode_directory, removed)
327 sourcecode_directory, new, possible_transports, tip, quiet)
328 updated = find_stale(updated, cache, sourcecode_directory, quiet)
329 update_branches(
330 sourcecode_directory, updated, possible_transports, tip, quiet)
331 changed = dict(updated)
332 changed.update(new)
333 update_cache(
334 cache, cache_filename, changed, sourcecode_directory, quiet)
335 remove_branches(sourcecode_directory, removed, quiet)
33687
33788
338# XXX: JonathanLange 2009-09-11: By default, the script will operate on the89# XXX: JonathanLange 2009-09-11: By default, the script will operate on the
@@ -349,9 +100,6 @@
349def main(args):100def main(args):
350 parser = optparse.OptionParser("usage: %prog [options] [root [conffile]]")101 parser = optparse.OptionParser("usage: %prog [options] [root [conffile]]")
351 parser.add_option(102 parser.add_option(
352 '--public-only', action='store_true',
353 help='Only fetch/update the public sourcecode branches.')
354 parser.add_option(
355 '--tip', action='store_true',103 '--tip', action='store_true',
356 help='Ignore revision constraints for all branches and pull tip')104 help='Ignore revision constraints for all branches and pull tip')
357 parser.add_option(105 parser.add_option(
@@ -374,20 +122,14 @@
374 config_filename = args[2]122 config_filename = args[2]
375 else:123 else:
376 config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')124 config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')
377 cache_filename = os.path.join(
378 root, 'utilities', 'sourcedeps.cache')
379 if len(args) > 3:125 if len(args) > 3:
380 parser.error("Too many arguments.")126 parser.error("Too many arguments.")
381 if not options.quiet:127 logging.basicConfig(
382 print 'Sourcecode: %s' % (sourcecode_directory,)128 format='%(message)s',
383 print 'Config: %s' % (config_filename,)129 level=logging.CRITICAL if options.quiet else logging.INFO)
384 enable_default_logging()130 logging.info('Sourcecode: %s', sourcecode_directory)
385 # Tell bzr to use the terminal (if any) to show progress bars131 logging.info('Config: %s', config_filename)
386 ui.ui_factory = ui.make_ui_for_terminal(
387 sys.stdin, sys.stdout, sys.stderr)
388 load_plugins()
389 update_sourcecode(132 update_sourcecode(
390 sourcecode_directory, config_filename, cache_filename,133 sourcecode_directory, config_filename,
391 options.public_only, options.tip, options.dry_run, options.quiet,134 tip=options.tip, dry_run=options.dry_run, use_http=options.use_http)
392 options.use_http)
393 return 0135 return 0
394136
=== modified file 'lib/devscripts/tests/test_sourcecode.py'
--- lib/devscripts/tests/test_sourcecode.py 2012-02-02 14:59:13 +0000
+++ lib/devscripts/tests/test_sourcecode.py 2017-11-01 09:54:39 +0000
@@ -1,221 +1,164 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Module docstring goes here."""4"""Module docstring goes here."""
55
6from __future__ import absolute_import, print_function, unicode_literals
7
6__metaclass__ = type8__metaclass__ = type
79
8import os10import os
9import shutil
10from StringIO import StringIO
11import tempfile11import tempfile
12import unittest
1312
14from bzrlib.bzrdir import BzrDir13from codetree.config import Config
15from bzrlib.tests import TestCase14from fixtures import (
16from bzrlib.transport import get_transport15 FakeLogger,
16 MonkeyPatch,
17 TempDir,
18 )
19import six
20from testtools import TestCase
1721
18from devscripts import get_launchpad_root22from devscripts import get_launchpad_root
19from devscripts.sourcecode import (23from devscripts.sourcecode import (
20 find_branches,24 find_branches,
21 interpret_config,25 mangle_config,
22 parse_config_file,
23 plan_update,26 plan_update,
27 update_sourcecode,
24 )28 )
2529
2630
27class TestParseConfigFile(unittest.TestCase):31def make_config(lines):
28 """Tests for the config file parser."""32 with tempfile.NamedTemporaryFile('w') as config_file:
2933 for line in lines:
30 def makeFile(self, contents):34 print(line, file=config_file)
31 return StringIO(contents)35 config_file.flush()
3236 return Config([config_file.name])
33 def test_empty(self):37
34 # Parsing an empty config file returns an empty sequence.38
35 empty_file = self.makeFile("")39class TestMangleConfig(TestCase):
36 self.assertEqual([], list(parse_config_file(empty_file)))40 """Tests for mangling configuration after codetree has parsed it."""
3741
38 def test_single_value(self):42 def setUp(self):
39 # Parsing a file containing a single key=value pair returns a sequence43 super(TestMangleConfig, self).setUp()
40 # containing the (key, value) as a list.44 self.tempdir = self.useFixture(TempDir()).path
41 config_file = self.makeFile("key value")45
42 self.assertEqual(46 def test_location(self):
43 [['key', 'value']], list(parse_config_file(config_file)))47 # All locations are considered to be relative to the given
4448 # sourcecode directory.
45 def test_comment_ignored(self):49 config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
46 # If a line begins with a '#', then its a comment.50 mangle_config(self.tempdir, config)
47 comment_only = self.makeFile('# foo')51 self.assertEqual(
48 self.assertEqual([], list(parse_config_file(comment_only)))52 os.path.join(self.tempdir, 'key'),
4953 config.directive_map['key'].location)
50 def test_optional_value(self):54
51 # Lines in the config file can have a third optional entry.55 def test_tip_false(self):
52 config_file = self.makeFile('key value optional')56 # If tip=False is passed to mangle_config, revno options are left
53 self.assertEqual(57 # untouched.
54 [['key', 'value', 'optional']],58 config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
55 list(parse_config_file(config_file)))59 mangle_config(self.tempdir, config, tip=False)
5660 self.assertEqual(
57 def test_whitespace_stripped(self):61 {'revno': '1'}, config.directive_map['key'].source_options)
58 # Any whitespace around any of the tokens in the config file are62
59 # stripped out.63 def test_tip_true(self):
60 config_file = self.makeFile(' key value optional ')64 # If tip=True is passed to mangle_config, revno options are removed.
61 self.assertEqual(65 config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
62 [['key', 'value', 'optional']],66 mangle_config(self.tempdir, config, tip=True)
63 list(parse_config_file(config_file)))67 self.assertEqual({}, config.directive_map['key'].source_options)
6468
6569 def test_use_http_false(self):
66class TestInterpretConfiguration(unittest.TestCase):70 # If use_http=False is passed to mangle_config, lp: branch URLs are
67 """Tests for the configuration interpreter."""71 # left untouched.
6872 url_path = '~sabdfl/foo/trunk'
69 def test_empty(self):73 config = make_config(['key lp:%s' % url_path])
70 # An empty configuration stream means no configuration.74 mangle_config(self.tempdir, config, use_http=False)
71 config = interpret_config([], False)75 self.assertEqual(
72 self.assertEqual({}, config)76 'lp:%s' % url_path, config.directive_map['key'].source.source)
7377
74 def test_key_value(self):78 def test_use_http_true(self):
75 # A (key, value) pair without a third optional value is returned in79 # If use_http=True is passed to mangle_config, lp: branch URLs are
76 # the configuration as a dictionary entry under 'key' with '(value,80 # transformed into http:// URLs.
77 # None, False)' as its value.81 url_path = '~sabdfl/foo/trunk'
78 config = interpret_config([['key', 'value']], False)82 config = make_config(['key lp:%s' % url_path])
79 self.assertEqual({'key': ('value', None, False)}, config)83 mangle_config(self.tempdir, config, use_http=True)
8084 self.assertEqual(
81 def test_key_value_public_only(self):85 'http://bazaar.launchpad.net/%s' % url_path,
82 # A (key, value) pair without a third optional value is returned in86 config.directive_map['key'].source.source)
83 # the configuration as a dictionary entry under 'key' with '(value,87
84 # None, False)' as its value when public_only is true.88
85 config = interpret_config([['key', 'value']], True)89class TestPlanUpdate(TestCase):
86 self.assertEqual({'key': ('value', None, False)}, config)
87
88 def test_key_value_optional(self):
89 # A (key, value, optional) entry is returned in the configuration as a
90 # dictionary entry under 'key' with '(value, True)' as its value.
91 config = interpret_config([['key', 'value', 'optional']], False)
92 self.assertEqual({'key': ('value', None, True)}, config)
93
94 def test_key_value_optional_public_only(self):
95 # A (key, value, optional) entry is not returned in the configuration
96 # when public_only is true.
97 config = interpret_config([['key', 'value', 'optional']], True)
98 self.assertEqual({}, config)
99
100 def test_key_value_revision(self):
101 # A (key, value) pair without a third optional value when the
102 # value has a suffix of ``;revno=[REVISION]`` is returned in the
103 # configuration as a dictionary entry under 'key' with '(value,
104 # None, False)' as its value.
105 config = interpret_config([['key', 'value;revno=45']], False)
106 self.assertEqual({'key': ('value', '45', False)}, config)
107
108 def test_key_value_revision(self):
109 # A (key, value) pair without a third optional value when the
110 # value has multiple suffixes of ``;revno=[REVISION]`` raises an
111 # error.
112 self.assertRaises(
113 AssertionError,
114 interpret_config, [['key', 'value;revno=45;revno=47']], False)
115
116 def test_too_many_values(self):
117 # A line with too many values raises an error.
118 self.assertRaises(
119 AssertionError,
120 interpret_config, [['key', 'value', 'optional', 'extra']], False)
121
122 def test_bad_optional_value(self):
123 # A third value that is not the "optional" string raises an error.
124 self.assertRaises(
125 AssertionError,
126 interpret_config, [['key', 'value', 'extra']], False)
127
128 def test_use_http(self):
129 # If use_http=True is passed to interpret_config, all lp: branch
130 # URLs will be transformed into http:// URLs.
131 config = interpret_config(
132 [['key', 'lp:~sabdfl/foo/trunk']], False, use_http=True)
133 expected_url = 'http://bazaar.launchpad.net/~sabdfl/foo/trunk'
134 self.assertEqual(expected_url, config['key'][0])
135
136
137class TestPlanUpdate(unittest.TestCase):
138 """Tests for how to plan the update."""90 """Tests for how to plan the update."""
13991
140 def test_trivial(self):92 def test_trivial(self):
141 # In the trivial case, there are no existing branches and no93 # In the trivial case, there are no existing branches and no
142 # configured branches, so there are no branches to add, none to94 # configured branches, so there are none to remove.
143 # update, and none to remove.95 removed = plan_update([], make_config([]))
144 new, existing, removed = plan_update([], {})
145 self.assertEqual({}, new)
146 self.assertEqual({}, existing)
147 self.assertEqual(set(), removed)96 self.assertEqual(set(), removed)
14897
149 def test_all_new(self):98 def test_all_new(self):
150 # If there are no existing branches, then the all of the configured99 # If there are no existing branches, then none have been removed.
151 # branches are new, none are existing and none have been removed.100 removed = plan_update([], make_config(['a lp:a']))
152 new, existing, removed = plan_update([], {'a': ('b', False)})
153 self.assertEqual({'a': ('b', False)}, new)
154 self.assertEqual({}, existing)
155 self.assertEqual(set(), removed)101 self.assertEqual(set(), removed)
156102
157 def test_all_old(self):103 def test_all_old(self):
158 # If there configuration is now empty, but there are existing104 # If the configuration is now empty but there are existing branches,
159 # branches, then that means all the branches have been removed from105 # then that means all the branches have been removed from the
160 # the configuration, none are new and none are updated.106 # configuration.
161 new, existing, removed = plan_update(['a', 'b', 'c'], {})107 removed = plan_update(['a', 'b', 'c'], make_config([]))
162 self.assertEqual({}, new)
163 self.assertEqual({}, existing)
164 self.assertEqual(set(['a', 'b', 'c']), removed)108 self.assertEqual(set(['a', 'b', 'c']), removed)
165109
166 def test_all_same(self):110 def test_all_same(self):
167 # If the set of existing branches is the same as the set of111 # If the set of existing branches is the same as the set of
168 # non-existing branches, then they all need to be updated.112 # non-existing branches, then none have been removed.
169 config = {'a': ('b', False), 'c': ('d', True)}113 config = make_config(['a lp:a', 'b lp:b'])
170 new, existing, removed = plan_update(config.keys(), config)114 removed = plan_update(config.directive_map.keys(), config)
171 self.assertEqual({}, new)
172 self.assertEqual(config, existing)
173 self.assertEqual(set(), removed)115 self.assertEqual(set(), removed)
174116
175 def test_smoke_the_default_config(self):117 def test_some_old(self):
176 # Make sure we can parse, interpret and plan based on the default118 # If there are existing branches not in the configuration, then
177 # config file.119 # those branches have been removed.
178 root = get_launchpad_root()120 removed = plan_update(['a', 'b', 'c'], make_config(['a lp:a']))
179 config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')121 self.assertEqual(set(['b', 'c']), removed)
180 config_file = open(config_filename)
181 config = interpret_config(parse_config_file(config_file), False)
182 config_file.close()
183 plan_update([], config)
184122
185123
186class TestFindBranches(TestCase):124class TestFindBranches(TestCase):
187 """Tests the way that we find branches."""125 """Tests the way that we find branches."""
188126
189 def setUp(self):127 def setUp(self):
190 TestCase.setUp(self)128 super(TestFindBranches, self).setUp()
191 self.disable_directory_isolation()129 self.tempdir = self.useFixture(TempDir()).path
192
193 def makeBranch(self, path):
194 transport = get_transport(path)
195 transport.ensure_base()
196 BzrDir.create_branch_convenience(
197 transport.base, possible_transports=[transport])
198
199 def makeDirectory(self):
200 directory = tempfile.mkdtemp()
201 self.addCleanup(shutil.rmtree, directory)
202 return directory
203130
204 def test_empty_directory_has_no_branches(self):131 def test_empty_directory_has_no_branches(self):
205 # An empty directory has no branches.132 # An empty directory has no branches.
206 empty = self.makeDirectory()133 self.assertEqual(set(), set(find_branches(self.tempdir)))
207 self.assertEqual([], list(find_branches(empty)))
208134
209 def test_directory_with_branches(self):135 def test_directory_with_branches(self):
210 # find_branches finds branches in the directory.136 # find_branches finds branches in the directory.
211 directory = self.makeDirectory()137 os.makedirs(os.path.join(self.tempdir, 'a', '.bzr'))
212 self.makeBranch('%s/a' % directory)138 os.makedirs(os.path.join(self.tempdir, 'b', '.git'))
213 self.assertEqual(['a'], list(find_branches(directory)))139 self.assertEqual(set(['a', 'b']), set(find_branches(self.tempdir)))
140
141 def test_ignores_non_branch_directory(self):
142 # find_branches ignores any subdirectories in the directory which
143 # are not branches.
144 os.mkdir(os.path.join(self.tempdir, 'a'))
145 self.assertEqual(set(), set(find_branches(self.tempdir)))
214146
215 def test_ignores_files(self):147 def test_ignores_files(self):
216 # find_branches ignores any files in the directory.148 # find_branches ignores any files in the directory.
217 directory = self.makeDirectory()149 with open(os.path.join(self.tempdir, 'a'), 'w') as some_file:
218 some_file = open('%s/a' % directory, 'w')150 some_file.write('hello\n')
219 some_file.write('hello\n')151 self.assertEqual(set(), set(find_branches(self.tempdir)))
220 some_file.close()152
221 self.assertEqual([], list(find_branches(directory)))153
154class TestSmoke(TestCase):
155 """Smoke tests."""
156
157 def test_smoke_the_default_config(self):
158 # Make sure we can do a dry run based on the default config file.
159 self.useFixture(FakeLogger())
160 self.useFixture(MonkeyPatch('sys.stdout', six.StringIO()))
161 root = get_launchpad_root()
162 config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')
163 fake_sourcecode = self.useFixture(TempDir()).path
164 update_sourcecode(fake_sourcecode, config_filename, dry_run=True)
222165
=== removed file 'utilities/sourcedeps.cache'
--- utilities/sourcedeps.cache 2016-03-30 08:16:52 +0000
+++ utilities/sourcedeps.cache 1970-01-01 00:00:00 +0000
@@ -1,70 +0,0 @@
1{
2 "bzr-builder": [
3 70,
4 "launchpad@pqm.canonical.com-20111114140506-6bmt9isw6lcud7yt"
5 ],
6 "bzr-git": [
7 279,
8 "launchpad@pqm.canonical.com-20130821040714-gy6puta8a2r1fkkg"
9 ],
10 "bzr-loom": [
11 55,
12 "launchpad@pqm.canonical.com-20120830090804-cg49kky93htwax7s"
13 ],
14 "bzr-svn": [
15 2725,
16 "launchpad@pqm.canonical.com-20130816045016-wzr810hu2z459t4y"
17 ],
18 "cscvs": [
19 433,
20 "launchpad@pqm.canonical.com-20130816043319-bts3l3bckmx431q1"
21 ],
22 "difftacular": [
23 6,
24 "aaron@aaronbentley.com-20100715135013-uoi3q430urx9gwb8"
25 ],
26 "dulwich": [
27 440,
28 "launchpad@pqm.canonical.com-20150619052709-6rmh11o0wdw09bzj"
29 ],
30 "loggerhead": [
31 490,
32 "william.grant@canonical.com-20160330070547-bbi1jc0pe6mqlquz"
33 ],
34 "lpreview": [
35 23,
36 "launchpad@pqm.canonical.com-20090720061538-euyh68ifavhy0pi8"
37 ],
38 "mailman": [
39 977,
40 "launchpad@pqm.canonical.com-20130405041235-9ud0xancja2eefd7"
41 ],
42 "mustache.js": [
43 166,
44 "git-v1:d87d274d4c37e3eb9ec28c2a5775d79bef4328c7"
45 ],
46 "old_xmlplus": [
47 4,
48 "sinzui-20090526164636-1swugzupwvjgomo4"
49 ],
50 "pygettextpo": [
51 25,
52 "launchpad@pqm.canonical.com-20140116030912-lqm1dtb6a0y4femq"
53 ],
54 "pygpgme": [
55 49,
56 "launchpad@pqm.canonical.com-20100325120516-q8to5dx3gga4wlvi"
57 ],
58 "python-debian": [
59 186,
60 "launchpad@pqm.canonical.com-20110329053617-irncjfr14k0m00zp"
61 ],
62 "subvertpy": [
63 2051,
64 "launchpad@pqm.canonical.com-20120627155818-0m7c94csgij9f1ee"
65 ],
66 "testresources": [
67 16,
68 "robertc@robertcollins.net-20050911111209-ee5da49011cf936a"
69 ]
70}
71\ No newline at end of file0\ No newline at end of file
721
=== removed file 'utilities/sourcedeps.filter'
--- utilities/sourcedeps.filter 2008-06-24 19:33:53 +0000
+++ utilities/sourcedeps.filter 1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
1P *.o
2P *.pyc
3P *.so