Merge lp:~cjwatson/launchpad/sourcedeps-codetree into lp:launchpad
- sourcedeps-codetree
- Merge into devel
Proposed by
Colin Watson
Status: | Rejected |
---|---|
Rejected by: | Colin Watson |
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+333073@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).
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Unmerged revisions
- 18498. By Colin Watson
-
Replace most of devscripts.
sourcecode with codetree. - 18497. By Colin Watson
-
Drop support for private/optional sourcedeps, unused since r12871.
- 18496. 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 | === 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 |
Superseded by https:/ /code.launchpad .net/~cjwatson/ launchpad/ +git/launchpad/ +merge/ 373737.