Merge ~cjwatson/launchpad:sourcedeps-codetree into launchpad:master
- Git
- lp:~cjwatson/launchpad
- sourcedeps-codetree
- Merge into master
Proposed by
Colin Watson
Status: | Rejected |
---|---|
Rejected by: | Colin Watson |
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+373737@code.launchpad.net |
Commit message
Replace most of devscripts.
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:/
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Unmerged commits
- 8c94b91... by Colin Watson
-
Replace most of devscripts.
sourcecode with codetree. - dc26ef8... by Colin Watson
-
Drop support for private/optional sourcedeps, unused since r12871.
- c6b5d56... by Colin Watson
-
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 | diff --git a/lib/devscripts/sourcecode.py b/lib/devscripts/sourcecode.py |
2 | index 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 |
423 | diff --git a/lib/devscripts/tests/test_sourcecode.py b/lib/devscripts/tests/test_sourcecode.py |
424 | index 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) |
759 | diff --git a/utilities/sourcedeps.cache b/utilities/sourcedeps.cache |
760 | deleted file mode 100644 |
761 | index 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 |
808 | diff --git a/utilities/sourcedeps.filter b/utilities/sourcedeps.filter |
809 | deleted file mode 100644 |
810 | index d81ca05..0000000 |
811 | --- a/utilities/sourcedeps.filter |
812 | +++ /dev/null |
813 | @@ -1,3 +0,0 @@ |
814 | -P *.o |
815 | -P *.pyc |
816 | -P *.so |
Superseded by https:/ /code.launchpad .net/~cjwatson/ launchpad/ +git/launchpad/ +merge/ 428729.