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
1=== modified file 'lib/devscripts/sourcecode.py'
2--- lib/devscripts/sourcecode.py 2012-06-25 12:21:10 +0000
3+++ lib/devscripts/sourcecode.py 2017-11-01 09:54:39 +0000
4@@ -1,136 +1,37 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Tools for maintaining the Launchpad source code."""
10
11+from __future__ import absolute_import, print_function, unicode_literals
12+
13 __metaclass__ = type
14 __all__ = [
15- 'interpret_config',
16- 'parse_config_file',
17- 'plan_update',
18+ 'main',
19 ]
20
21-import errno
22-import json
23+import logging
24 import optparse
25 import os
26 import shutil
27-import sys
28
29-from bzrlib import ui
30-from bzrlib.branch import Branch
31-from bzrlib.errors import (
32- BzrError,
33- IncompatibleRepositories,
34- NotBranchError,
35- )
36-from bzrlib.plugin import load_plugins
37-from bzrlib.revisionspec import RevisionSpec
38-from bzrlib.trace import (
39- enable_default_logging,
40- report_exception,
41- )
42-from bzrlib.upgrade import upgrade
43-from bzrlib.workingtree import WorkingTree
44+from codetree.config import Config
45+from codetree.handlers.bzr import BzrSourceHandler
46
47 from devscripts import get_launchpad_root
48
49
50-def parse_config_file(file_handle):
51- """Parse the source code config file 'file_handle'.
52-
53- :param file_handle: A file-like object containing sourcecode
54- configuration.
55- :return: A sequence of lines of either '[key, value]' or
56- '[key, value, optional]'.
57- """
58- for line in file_handle:
59- if line == '\n' or line.startswith('#'):
60- continue
61- yield line.split()
62-
63-
64-def interpret_config_entry(entry, use_http=False):
65- """Interpret a single parsed line from the config file."""
66- branch_name = entry[0]
67- components = entry[1].split(';revno=')
68- branch_url = components[0]
69- if use_http:
70- branch_url = branch_url.replace('lp:', 'http://bazaar.launchpad.net/')
71- if len(components) == 1:
72- revision = None
73- else:
74- assert len(components) == 2, 'Bad branch URL: ' + entry[1]
75- revision = components[1] or None
76- if len(entry) > 2:
77- assert len(entry) == 3 and entry[2].lower() == 'optional', (
78- 'Bad configuration line: should be space delimited values of '
79- 'sourcecode directory name, branch URL [, "optional"]\n' +
80- ' '.join(entry))
81- optional = True
82- else:
83- optional = False
84- return branch_name, branch_url, revision, optional
85-
86-
87-def load_cache(cache_filename):
88- try:
89- cache_file = open(cache_filename, 'rb')
90- except IOError as e:
91- if e.errno == errno.ENOENT:
92- return {}
93- else:
94- raise
95- with cache_file:
96- return json.load(cache_file)
97-
98-
99-def interpret_config(config_entries, public_only, use_http=False):
100- """Interpret a configuration stream, as parsed by 'parse_config_file'.
101-
102- :param configuration: A sequence of parsed configuration entries.
103- :param public_only: If true, ignore private/optional branches.
104- :param use_http: If True, force all branch URLs to use http://
105- :return: A dict mapping the names of the sourcecode dependencies to a
106- 2-tuple of their branches and whether or not they are optional.
107- """
108- config = {}
109- for entry in config_entries:
110- branch_name, branch_url, revision, optional = interpret_config_entry(
111- entry, use_http)
112- if not optional or not public_only:
113- config[branch_name] = (branch_url, revision, optional)
114- return config
115-
116-
117-def _subset_dict(d, keys):
118- """Return a dict that's a subset of 'd', based on the keys in 'keys'."""
119- return dict((key, d[key]) for key in keys)
120-
121-
122-def plan_update(existing_branches, configuration):
123+def plan_update(existing_branches, config):
124 """Plan the update to existing branches based on 'configuration'.
125
126 :param existing_branches: A sequence of branches that already exist.
127- :param configuration: A dictionary of sourcecode configuration, such as is
128- returned by `interpret_config`.
129- :return: (new_branches, update_branches, removed_branches), where
130- 'new_branches' are the branches in the configuration that don't exist
131- yet, 'update_branches' are the branches in the configuration that do
132- exist, and 'removed_branches' are the branches that exist locally, but
133- not in the configuration. 'new_branches' and 'update_branches' are
134- dicts of the same form as 'configuration', 'removed_branches' is a
135- set of the same form as 'existing_branches'.
136+ :param config: An instance of `codetree.Config`.
137+ :return: a set of the branches that exist locally but not in the
138+ configuration.
139 """
140 existing_branches = set(existing_branches)
141- config_branches = set(configuration.keys())
142- new_branches = config_branches - existing_branches
143- removed_branches = existing_branches - config_branches
144- update_branches = config_branches.intersection(existing_branches)
145- return (
146- _subset_dict(configuration, new_branches),
147- _subset_dict(configuration, update_branches),
148- removed_branches)
149+ config_branches = set(config.directive_map)
150+ return existing_branches - config_branches
151
152
153 def find_branches(directory):
154@@ -139,200 +40,50 @@
155 for name in os.listdir(directory):
156 if name in ('.', '..'):
157 continue
158- try:
159- Branch.open(os.path.join(directory, name))
160- branches.append(name)
161- except NotBranchError:
162- pass
163+ for subdir in ('.bzr', '.git'):
164+ if os.path.exists(os.path.join(directory, name, subdir)):
165+ branches.append(name)
166 return branches
167
168
169-def get_revision_id(revision, from_branch, tip=False):
170- """Return revision id for a revision number and a branch.
171-
172- If the revision is empty, the revision_id will be None.
173-
174- If ``tip`` is True, the revision value will be ignored.
175- """
176- if not tip and revision:
177- spec = RevisionSpec.from_string(revision)
178- return spec.as_revision_id(from_branch)
179- # else return None
180-
181-
182-def _format_revision_name(revision, tip=False):
183- """Formatting helper to return human-readable identifier for revision.
184-
185- If ``tip`` is True, the revision value will be ignored.
186- """
187- if not tip and revision:
188- return 'revision %s' % (revision,)
189- else:
190- return 'tip'
191-
192-
193-def get_branches(sourcecode_directory, new_branches,
194- possible_transports=None, tip=False, quiet=False):
195- """Get the new branches into sourcecode."""
196- for project, (branch_url, revision, optional) in new_branches.iteritems():
197- destination = os.path.join(sourcecode_directory, project)
198- try:
199- remote_branch = Branch.open(
200- branch_url, possible_transports=possible_transports)
201- except BzrError:
202- if optional:
203- report_exception(sys.exc_info(), sys.stderr)
204- continue
205- else:
206- raise
207- possible_transports.append(
208- remote_branch.bzrdir.root_transport)
209- if not quiet:
210- print 'Getting %s from %s at %s' % (
211- project, branch_url, _format_revision_name(revision, tip))
212- # If the 'optional' flag is set, then it's a branch that shares
213- # history with Launchpad, so we should share repositories. Otherwise,
214- # we should avoid sharing repositories to avoid format
215- # incompatibilities.
216- force_new_repo = not optional
217- revision_id = get_revision_id(revision, remote_branch, tip)
218- remote_branch.bzrdir.sprout(
219- destination, revision_id=revision_id, create_tree_if_local=True,
220- source_branch=remote_branch, force_new_repo=force_new_repo,
221- possible_transports=possible_transports)
222-
223-
224-def find_stale(updated, cache, sourcecode_directory, quiet):
225- """Find branches whose revision info doesn't match the cache."""
226- new_updated = dict(updated)
227- for project, (branch_url, revision, optional) in updated.iteritems():
228- cache_revision_info = cache.get(project)
229- if cache_revision_info is None:
230- continue
231- if cache_revision_info[0] != int(revision):
232- continue
233- destination = os.path.join(sourcecode_directory, project)
234- try:
235- branch = Branch.open(destination)
236- except BzrError:
237- continue
238- if list(branch.last_revision_info()) != cache_revision_info:
239- continue
240- if not quiet:
241- print '%s is already up to date.' % project
242- del new_updated[project]
243- return new_updated
244-
245-
246-def update_cache(cache, cache_filename, changed, sourcecode_directory, quiet):
247- """Update the cache with the changed branches."""
248- old_cache = dict(cache)
249- for project, (branch_url, revision, optional) in changed.iteritems():
250- destination = os.path.join(sourcecode_directory, project)
251- branch = Branch.open(destination)
252- cache[project] = list(branch.last_revision_info())
253- if cache == old_cache:
254- return
255- with open(cache_filename, 'wb') as cache_file:
256- json.dump(cache, cache_file, indent=4, sort_keys=True)
257- if not quiet:
258- print 'Cache updated. Please commit "%s".' % cache_filename
259-
260-
261-def update_branches(sourcecode_directory, update_branches,
262- possible_transports=None, tip=False, quiet=False):
263- """Update the existing branches in sourcecode."""
264- if possible_transports is None:
265- possible_transports = []
266- # XXX: JonathanLange 2009-11-09: Rather than updating one branch after
267- # another, we could instead try to get them in parallel.
268- for project, (branch_url, revision, optional) in (
269- update_branches.iteritems()):
270- # Update project from branch_url.
271- destination = os.path.join(sourcecode_directory, project)
272- if not quiet:
273- print 'Updating %s to %s' % (
274- project, _format_revision_name(revision, tip))
275- local_tree = WorkingTree.open(destination)
276- try:
277- remote_branch = Branch.open(
278- branch_url, possible_transports=possible_transports)
279- except BzrError:
280- if optional:
281- report_exception(sys.exc_info(), sys.stderr)
282- continue
283- else:
284- raise
285- possible_transports.append(
286- remote_branch.bzrdir.root_transport)
287- revision_id = get_revision_id(revision, remote_branch, tip)
288- try:
289- result = local_tree.pull(
290- remote_branch, stop_revision=revision_id, overwrite=True,
291- possible_transports=possible_transports)
292- except IncompatibleRepositories:
293- # XXX JRV 20100407: Ideally remote_branch.bzrdir._format
294- # should be passed into upgrade() to ensure the format is the same
295- # locally and remotely. Unfortunately smart server branches
296- # have their _format set to RemoteFormat rather than an actual
297- # format instance.
298- upgrade(destination)
299- # Upgraded, repoen working tree
300- local_tree = WorkingTree.open(destination)
301- result = local_tree.pull(
302- remote_branch, stop_revision=revision_id, overwrite=True,
303- possible_transports=possible_transports)
304- if result.old_revid == result.new_revid:
305- if not quiet:
306- print ' (No change)'
307- else:
308- if result.old_revno < result.new_revno:
309- change = 'Updated'
310- else:
311- change = 'Reverted'
312- if not quiet:
313- print ' (%s from %s to %s)' % (
314- change, result.old_revno, result.new_revno)
315-
316-
317-def remove_branches(sourcecode_directory, removed_branches, quiet=False):
318+def remove_branches(sourcecode_directory, removed_branches):
319 """Remove sourcecode that's no longer there."""
320 for project in removed_branches:
321 destination = os.path.join(sourcecode_directory, project)
322- if not quiet:
323- print 'Removing %s' % project
324+ logging.info('Removing %s', project)
325 try:
326 shutil.rmtree(destination)
327 except OSError:
328 os.unlink(destination)
329
330
331-def update_sourcecode(sourcecode_directory, config_filename, cache_filename,
332- public_only, tip, dry_run, quiet=False, use_http=False):
333- """Update the sourcecode."""
334- config_file = open(config_filename)
335- config = interpret_config(
336- parse_config_file(config_file), public_only, use_http)
337- config_file.close()
338- cache = load_cache(cache_filename)
339+def mangle_config(sourcecode_directory, config, tip=False, use_http=False):
340+ for directive in config.directive_map.values():
341+ directive.location = os.path.join(
342+ sourcecode_directory, directive.location)
343+ if tip:
344+ directive.source_options.pop('revno', None)
345+ if use_http:
346+ handler = directive.source
347+ if (isinstance(handler, BzrSourceHandler) and
348+ handler.source.startswith('lp:')):
349+ handler.source = handler.source.replace(
350+ 'lp:', 'http://bazaar.launchpad.net/')
351+
352+
353+def update_sourcecode(sourcecode_directory, config_filename,
354+ tip=False, dry_run=False, use_http=False):
355+ config = Config([config_filename])
356+ mangle_config(sourcecode_directory, config, tip=tip, use_http=use_http)
357 branches = find_branches(sourcecode_directory)
358- new, updated, removed = plan_update(branches, config)
359- possible_transports = []
360+ removed = plan_update(branches, config)
361+ # XXX cjwatson 2017-10-31: If we start pulling sourcedeps from git, then
362+ # we need to remove old bzr branches first.
363+ config.build(dry_run=dry_run)
364 if dry_run:
365- print 'Branches to fetch:', new.keys()
366- print 'Branches to update:', updated.keys()
367- print 'Branches to remove:', list(removed)
368+ logging.info('Branches to remove: %s', list(removed))
369 else:
370- get_branches(
371- sourcecode_directory, new, possible_transports, tip, quiet)
372- updated = find_stale(updated, cache, sourcecode_directory, quiet)
373- update_branches(
374- sourcecode_directory, updated, possible_transports, tip, quiet)
375- changed = dict(updated)
376- changed.update(new)
377- update_cache(
378- cache, cache_filename, changed, sourcecode_directory, quiet)
379- remove_branches(sourcecode_directory, removed, quiet)
380+ remove_branches(sourcecode_directory, removed)
381
382
383 # XXX: JonathanLange 2009-09-11: By default, the script will operate on the
384@@ -349,9 +100,6 @@
385 def main(args):
386 parser = optparse.OptionParser("usage: %prog [options] [root [conffile]]")
387 parser.add_option(
388- '--public-only', action='store_true',
389- help='Only fetch/update the public sourcecode branches.')
390- parser.add_option(
391 '--tip', action='store_true',
392 help='Ignore revision constraints for all branches and pull tip')
393 parser.add_option(
394@@ -374,20 +122,14 @@
395 config_filename = args[2]
396 else:
397 config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')
398- cache_filename = os.path.join(
399- root, 'utilities', 'sourcedeps.cache')
400 if len(args) > 3:
401 parser.error("Too many arguments.")
402- if not options.quiet:
403- print 'Sourcecode: %s' % (sourcecode_directory,)
404- print 'Config: %s' % (config_filename,)
405- enable_default_logging()
406- # Tell bzr to use the terminal (if any) to show progress bars
407- ui.ui_factory = ui.make_ui_for_terminal(
408- sys.stdin, sys.stdout, sys.stderr)
409- load_plugins()
410+ logging.basicConfig(
411+ format='%(message)s',
412+ level=logging.CRITICAL if options.quiet else logging.INFO)
413+ logging.info('Sourcecode: %s', sourcecode_directory)
414+ logging.info('Config: %s', config_filename)
415 update_sourcecode(
416- sourcecode_directory, config_filename, cache_filename,
417- options.public_only, options.tip, options.dry_run, options.quiet,
418- options.use_http)
419+ sourcecode_directory, config_filename,
420+ tip=options.tip, dry_run=options.dry_run, use_http=options.use_http)
421 return 0
422
423=== modified file 'lib/devscripts/tests/test_sourcecode.py'
424--- lib/devscripts/tests/test_sourcecode.py 2012-02-02 14:59:13 +0000
425+++ lib/devscripts/tests/test_sourcecode.py 2017-11-01 09:54:39 +0000
426@@ -1,221 +1,164 @@
427-# Copyright 2009 Canonical Ltd. This software is licensed under the
428+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
429 # GNU Affero General Public License version 3 (see the file LICENSE).
430
431 """Module docstring goes here."""
432
433+from __future__ import absolute_import, print_function, unicode_literals
434+
435 __metaclass__ = type
436
437 import os
438-import shutil
439-from StringIO import StringIO
440 import tempfile
441-import unittest
442
443-from bzrlib.bzrdir import BzrDir
444-from bzrlib.tests import TestCase
445-from bzrlib.transport import get_transport
446+from codetree.config import Config
447+from fixtures import (
448+ FakeLogger,
449+ MonkeyPatch,
450+ TempDir,
451+ )
452+import six
453+from testtools import TestCase
454
455 from devscripts import get_launchpad_root
456 from devscripts.sourcecode import (
457 find_branches,
458- interpret_config,
459- parse_config_file,
460+ mangle_config,
461 plan_update,
462+ update_sourcecode,
463 )
464
465
466-class TestParseConfigFile(unittest.TestCase):
467- """Tests for the config file parser."""
468-
469- def makeFile(self, contents):
470- return StringIO(contents)
471-
472- def test_empty(self):
473- # Parsing an empty config file returns an empty sequence.
474- empty_file = self.makeFile("")
475- self.assertEqual([], list(parse_config_file(empty_file)))
476-
477- def test_single_value(self):
478- # Parsing a file containing a single key=value pair returns a sequence
479- # containing the (key, value) as a list.
480- config_file = self.makeFile("key value")
481- self.assertEqual(
482- [['key', 'value']], list(parse_config_file(config_file)))
483-
484- def test_comment_ignored(self):
485- # If a line begins with a '#', then its a comment.
486- comment_only = self.makeFile('# foo')
487- self.assertEqual([], list(parse_config_file(comment_only)))
488-
489- def test_optional_value(self):
490- # Lines in the config file can have a third optional entry.
491- config_file = self.makeFile('key value optional')
492- self.assertEqual(
493- [['key', 'value', 'optional']],
494- list(parse_config_file(config_file)))
495-
496- def test_whitespace_stripped(self):
497- # Any whitespace around any of the tokens in the config file are
498- # stripped out.
499- config_file = self.makeFile(' key value optional ')
500- self.assertEqual(
501- [['key', 'value', 'optional']],
502- list(parse_config_file(config_file)))
503-
504-
505-class TestInterpretConfiguration(unittest.TestCase):
506- """Tests for the configuration interpreter."""
507-
508- def test_empty(self):
509- # An empty configuration stream means no configuration.
510- config = interpret_config([], False)
511- self.assertEqual({}, config)
512-
513- def test_key_value(self):
514- # A (key, value) pair without a third optional value is returned in
515- # the configuration as a dictionary entry under 'key' with '(value,
516- # None, False)' as its value.
517- config = interpret_config([['key', 'value']], False)
518- self.assertEqual({'key': ('value', None, False)}, config)
519-
520- def test_key_value_public_only(self):
521- # A (key, value) pair without a third optional value is returned in
522- # the configuration as a dictionary entry under 'key' with '(value,
523- # None, False)' as its value when public_only is true.
524- config = interpret_config([['key', 'value']], True)
525- self.assertEqual({'key': ('value', None, False)}, config)
526-
527- def test_key_value_optional(self):
528- # A (key, value, optional) entry is returned in the configuration as a
529- # dictionary entry under 'key' with '(value, True)' as its value.
530- config = interpret_config([['key', 'value', 'optional']], False)
531- self.assertEqual({'key': ('value', None, True)}, config)
532-
533- def test_key_value_optional_public_only(self):
534- # A (key, value, optional) entry is not returned in the configuration
535- # when public_only is true.
536- config = interpret_config([['key', 'value', 'optional']], True)
537- self.assertEqual({}, config)
538-
539- def test_key_value_revision(self):
540- # A (key, value) pair without a third optional value when the
541- # value has a suffix of ``;revno=[REVISION]`` is returned in the
542- # configuration as a dictionary entry under 'key' with '(value,
543- # None, False)' as its value.
544- config = interpret_config([['key', 'value;revno=45']], False)
545- self.assertEqual({'key': ('value', '45', False)}, config)
546-
547- def test_key_value_revision(self):
548- # A (key, value) pair without a third optional value when the
549- # value has multiple suffixes of ``;revno=[REVISION]`` raises an
550- # error.
551- self.assertRaises(
552- AssertionError,
553- interpret_config, [['key', 'value;revno=45;revno=47']], False)
554-
555- def test_too_many_values(self):
556- # A line with too many values raises an error.
557- self.assertRaises(
558- AssertionError,
559- interpret_config, [['key', 'value', 'optional', 'extra']], False)
560-
561- def test_bad_optional_value(self):
562- # A third value that is not the "optional" string raises an error.
563- self.assertRaises(
564- AssertionError,
565- interpret_config, [['key', 'value', 'extra']], False)
566-
567- def test_use_http(self):
568- # If use_http=True is passed to interpret_config, all lp: branch
569- # URLs will be transformed into http:// URLs.
570- config = interpret_config(
571- [['key', 'lp:~sabdfl/foo/trunk']], False, use_http=True)
572- expected_url = 'http://bazaar.launchpad.net/~sabdfl/foo/trunk'
573- self.assertEqual(expected_url, config['key'][0])
574-
575-
576-class TestPlanUpdate(unittest.TestCase):
577+def make_config(lines):
578+ with tempfile.NamedTemporaryFile('w') as config_file:
579+ for line in lines:
580+ print(line, file=config_file)
581+ config_file.flush()
582+ return Config([config_file.name])
583+
584+
585+class TestMangleConfig(TestCase):
586+ """Tests for mangling configuration after codetree has parsed it."""
587+
588+ def setUp(self):
589+ super(TestMangleConfig, self).setUp()
590+ self.tempdir = self.useFixture(TempDir()).path
591+
592+ def test_location(self):
593+ # All locations are considered to be relative to the given
594+ # sourcecode directory.
595+ config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
596+ mangle_config(self.tempdir, config)
597+ self.assertEqual(
598+ os.path.join(self.tempdir, 'key'),
599+ config.directive_map['key'].location)
600+
601+ def test_tip_false(self):
602+ # If tip=False is passed to mangle_config, revno options are left
603+ # untouched.
604+ config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
605+ mangle_config(self.tempdir, config, tip=False)
606+ self.assertEqual(
607+ {'revno': '1'}, config.directive_map['key'].source_options)
608+
609+ def test_tip_true(self):
610+ # If tip=True is passed to mangle_config, revno options are removed.
611+ config = make_config(['key lp:~sabdfl/foo/trunk;revno=1'])
612+ mangle_config(self.tempdir, config, tip=True)
613+ self.assertEqual({}, config.directive_map['key'].source_options)
614+
615+ def test_use_http_false(self):
616+ # If use_http=False is passed to mangle_config, lp: branch URLs are
617+ # left untouched.
618+ url_path = '~sabdfl/foo/trunk'
619+ config = make_config(['key lp:%s' % url_path])
620+ mangle_config(self.tempdir, config, use_http=False)
621+ self.assertEqual(
622+ 'lp:%s' % url_path, config.directive_map['key'].source.source)
623+
624+ def test_use_http_true(self):
625+ # If use_http=True is passed to mangle_config, lp: branch URLs are
626+ # transformed into http:// URLs.
627+ url_path = '~sabdfl/foo/trunk'
628+ config = make_config(['key lp:%s' % url_path])
629+ mangle_config(self.tempdir, config, use_http=True)
630+ self.assertEqual(
631+ 'http://bazaar.launchpad.net/%s' % url_path,
632+ config.directive_map['key'].source.source)
633+
634+
635+class TestPlanUpdate(TestCase):
636 """Tests for how to plan the update."""
637
638 def test_trivial(self):
639 # In the trivial case, there are no existing branches and no
640- # configured branches, so there are no branches to add, none to
641- # update, and none to remove.
642- new, existing, removed = plan_update([], {})
643- self.assertEqual({}, new)
644- self.assertEqual({}, existing)
645+ # configured branches, so there are none to remove.
646+ removed = plan_update([], make_config([]))
647 self.assertEqual(set(), removed)
648
649 def test_all_new(self):
650- # If there are no existing branches, then the all of the configured
651- # branches are new, none are existing and none have been removed.
652- new, existing, removed = plan_update([], {'a': ('b', False)})
653- self.assertEqual({'a': ('b', False)}, new)
654- self.assertEqual({}, existing)
655+ # If there are no existing branches, then none have been removed.
656+ removed = plan_update([], make_config(['a lp:a']))
657 self.assertEqual(set(), removed)
658
659 def test_all_old(self):
660- # If there configuration is now empty, but there are existing
661- # branches, then that means all the branches have been removed from
662- # the configuration, none are new and none are updated.
663- new, existing, removed = plan_update(['a', 'b', 'c'], {})
664- self.assertEqual({}, new)
665- self.assertEqual({}, existing)
666+ # If the configuration is now empty but there are existing branches,
667+ # then that means all the branches have been removed from the
668+ # configuration.
669+ removed = plan_update(['a', 'b', 'c'], make_config([]))
670 self.assertEqual(set(['a', 'b', 'c']), removed)
671
672 def test_all_same(self):
673 # If the set of existing branches is the same as the set of
674- # non-existing branches, then they all need to be updated.
675- config = {'a': ('b', False), 'c': ('d', True)}
676- new, existing, removed = plan_update(config.keys(), config)
677- self.assertEqual({}, new)
678- self.assertEqual(config, existing)
679+ # non-existing branches, then none have been removed.
680+ config = make_config(['a lp:a', 'b lp:b'])
681+ removed = plan_update(config.directive_map.keys(), config)
682 self.assertEqual(set(), removed)
683
684- def test_smoke_the_default_config(self):
685- # Make sure we can parse, interpret and plan based on the default
686- # config file.
687- root = get_launchpad_root()
688- config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')
689- config_file = open(config_filename)
690- config = interpret_config(parse_config_file(config_file), False)
691- config_file.close()
692- plan_update([], config)
693+ def test_some_old(self):
694+ # If there are existing branches not in the configuration, then
695+ # those branches have been removed.
696+ removed = plan_update(['a', 'b', 'c'], make_config(['a lp:a']))
697+ self.assertEqual(set(['b', 'c']), removed)
698
699
700 class TestFindBranches(TestCase):
701 """Tests the way that we find branches."""
702
703 def setUp(self):
704- TestCase.setUp(self)
705- self.disable_directory_isolation()
706-
707- def makeBranch(self, path):
708- transport = get_transport(path)
709- transport.ensure_base()
710- BzrDir.create_branch_convenience(
711- transport.base, possible_transports=[transport])
712-
713- def makeDirectory(self):
714- directory = tempfile.mkdtemp()
715- self.addCleanup(shutil.rmtree, directory)
716- return directory
717+ super(TestFindBranches, self).setUp()
718+ self.tempdir = self.useFixture(TempDir()).path
719
720 def test_empty_directory_has_no_branches(self):
721 # An empty directory has no branches.
722- empty = self.makeDirectory()
723- self.assertEqual([], list(find_branches(empty)))
724+ self.assertEqual(set(), set(find_branches(self.tempdir)))
725
726 def test_directory_with_branches(self):
727 # find_branches finds branches in the directory.
728- directory = self.makeDirectory()
729- self.makeBranch('%s/a' % directory)
730- self.assertEqual(['a'], list(find_branches(directory)))
731+ os.makedirs(os.path.join(self.tempdir, 'a', '.bzr'))
732+ os.makedirs(os.path.join(self.tempdir, 'b', '.git'))
733+ self.assertEqual(set(['a', 'b']), set(find_branches(self.tempdir)))
734+
735+ def test_ignores_non_branch_directory(self):
736+ # find_branches ignores any subdirectories in the directory which
737+ # are not branches.
738+ os.mkdir(os.path.join(self.tempdir, 'a'))
739+ self.assertEqual(set(), set(find_branches(self.tempdir)))
740
741 def test_ignores_files(self):
742 # find_branches ignores any files in the directory.
743- directory = self.makeDirectory()
744- some_file = open('%s/a' % directory, 'w')
745- some_file.write('hello\n')
746- some_file.close()
747- self.assertEqual([], list(find_branches(directory)))
748+ with open(os.path.join(self.tempdir, 'a'), 'w') as some_file:
749+ some_file.write('hello\n')
750+ self.assertEqual(set(), set(find_branches(self.tempdir)))
751+
752+
753+class TestSmoke(TestCase):
754+ """Smoke tests."""
755+
756+ def test_smoke_the_default_config(self):
757+ # Make sure we can do a dry run based on the default config file.
758+ self.useFixture(FakeLogger())
759+ self.useFixture(MonkeyPatch('sys.stdout', six.StringIO()))
760+ root = get_launchpad_root()
761+ config_filename = os.path.join(root, 'utilities', 'sourcedeps.conf')
762+ fake_sourcecode = self.useFixture(TempDir()).path
763+ update_sourcecode(fake_sourcecode, config_filename, dry_run=True)
764
765=== removed file 'utilities/sourcedeps.cache'
766--- utilities/sourcedeps.cache 2016-03-30 08:16:52 +0000
767+++ utilities/sourcedeps.cache 1970-01-01 00:00:00 +0000
768@@ -1,70 +0,0 @@
769-{
770- "bzr-builder": [
771- 70,
772- "launchpad@pqm.canonical.com-20111114140506-6bmt9isw6lcud7yt"
773- ],
774- "bzr-git": [
775- 279,
776- "launchpad@pqm.canonical.com-20130821040714-gy6puta8a2r1fkkg"
777- ],
778- "bzr-loom": [
779- 55,
780- "launchpad@pqm.canonical.com-20120830090804-cg49kky93htwax7s"
781- ],
782- "bzr-svn": [
783- 2725,
784- "launchpad@pqm.canonical.com-20130816045016-wzr810hu2z459t4y"
785- ],
786- "cscvs": [
787- 433,
788- "launchpad@pqm.canonical.com-20130816043319-bts3l3bckmx431q1"
789- ],
790- "difftacular": [
791- 6,
792- "aaron@aaronbentley.com-20100715135013-uoi3q430urx9gwb8"
793- ],
794- "dulwich": [
795- 440,
796- "launchpad@pqm.canonical.com-20150619052709-6rmh11o0wdw09bzj"
797- ],
798- "loggerhead": [
799- 490,
800- "william.grant@canonical.com-20160330070547-bbi1jc0pe6mqlquz"
801- ],
802- "lpreview": [
803- 23,
804- "launchpad@pqm.canonical.com-20090720061538-euyh68ifavhy0pi8"
805- ],
806- "mailman": [
807- 977,
808- "launchpad@pqm.canonical.com-20130405041235-9ud0xancja2eefd7"
809- ],
810- "mustache.js": [
811- 166,
812- "git-v1:d87d274d4c37e3eb9ec28c2a5775d79bef4328c7"
813- ],
814- "old_xmlplus": [
815- 4,
816- "sinzui-20090526164636-1swugzupwvjgomo4"
817- ],
818- "pygettextpo": [
819- 25,
820- "launchpad@pqm.canonical.com-20140116030912-lqm1dtb6a0y4femq"
821- ],
822- "pygpgme": [
823- 49,
824- "launchpad@pqm.canonical.com-20100325120516-q8to5dx3gga4wlvi"
825- ],
826- "python-debian": [
827- 186,
828- "launchpad@pqm.canonical.com-20110329053617-irncjfr14k0m00zp"
829- ],
830- "subvertpy": [
831- 2051,
832- "launchpad@pqm.canonical.com-20120627155818-0m7c94csgij9f1ee"
833- ],
834- "testresources": [
835- 16,
836- "robertc@robertcollins.net-20050911111209-ee5da49011cf936a"
837- ]
838-}
839\ No newline at end of file
840
841=== removed file 'utilities/sourcedeps.filter'
842--- utilities/sourcedeps.filter 2008-06-24 19:33:53 +0000
843+++ utilities/sourcedeps.filter 1970-01-01 00:00:00 +0000
844@@ -1,3 +0,0 @@
845-P *.o
846-P *.pyc
847-P *.so