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

Proposed by Colin Watson on 2019-10-07
Status: Needs review
Proposed branch: ~cjwatson/launchpad:sourcedeps-codetree
Merge into: launchpad:master
Diff against target: 816 lines (+159/-477)
3 files modified
dev/null (+0/-3)
lib/devscripts/sourcecode.py (+49/-307)
lib/devscripts/tests/test_sourcecode.py (+110/-167)
Reviewer Review Type Date Requested Status
Launchpad code reviewers 2019-10-07 Pending
Review via email: mp+373737@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).

This is essentially the same as https://code.launchpad.net/~cjwatson/launchpad/sourcedeps-codetree/+merge/333073, converted to git and rebased on master.

To post a comment you must log in.

Unmerged commits

8c94b91... by Colin Watson on 2017-11-01

Replace most of devscripts.sourcecode with codetree.

dc26ef8... by Colin Watson on 2017-10-31

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

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

Subscribers

People subscribed via source and target branches