Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8)

Side by Side Diff: recipe_engine/package.py

Issue 2664223002: Add unit tests for overrides in multi-repo workflow (Closed)
Patch Set: reverts Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | recipes.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2015 The LUCI Authors. All rights reserved. 1 # Copyright 2015 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 import ast 5 import ast
6 import collections 6 import collections
7 import contextlib 7 import contextlib
8 import copy 8 import copy
9 import difflib 9 import difflib
10 import functools 10 import functools
11 import itertools 11 import itertools
12 import logging 12 import logging
13 import operator 13 import operator
14 import os 14 import os
15 import subprocess 15 import subprocess
16 import sys 16 import sys
17 import tempfile 17 import tempfile
18 18
19 from . import env 19 from . import env
20 20
21 from google.protobuf import text_format 21 from google.protobuf import text_format
22 from . import package_pb2 22 from . import package_pb2
23 from . import fetch 23 from . import fetch
24 24
25 25
26 class InconsistentDependencyGraphError(Exception): 26 class InconsistentDependencyGraphError(Exception):
27 def __init__(self, project_id, specs): 27 def __init__(self, project_id, specs):
28 super(InconsistentDependencyGraphError, self).__init__(
29 'Package specs for %s do not match: %s vs %s' % (
30 project_id, specs[0], specs[1]))
31 self.project_id = project_id 28 self.project_id = project_id
32 self.specs = specs 29 self.specs = specs
33 30
31 def __str__(self):
32 return 'Package specs for %s do not match: %s vs %s' % (
33 project_id, self.specs[0], self.specs[1])
34
34 35
35 class CyclicDependencyError(Exception): 36 class CyclicDependencyError(Exception):
36 pass 37 pass
37 38
38 39
39 def cleanup_pyc(path): 40 def cleanup_pyc(path):
40 """Removes any .pyc files from |path|'s directory tree. 41 """Removes any .pyc files from |path|'s directory tree.
41 42
42 This ensures we always use the fresh code. 43 This ensures we always use the fresh code.
43 """ 44 """
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after
151 def checkout(self, context): 152 def checkout(self, context):
152 """Fetches the specified package and returns the path of the package root 153 """Fetches the specified package and returns the path of the package root
153 (the directory that contains recipes and recipe_modules). 154 (the directory that contains recipes and recipe_modules).
154 """ 155 """
155 raise NotImplementedError() 156 raise NotImplementedError()
156 157
157 def repo_root(self, context): 158 def repo_root(self, context):
158 """Returns the root of this repository.""" 159 """Returns the root of this repository."""
159 raise NotImplementedError() 160 raise NotImplementedError()
160 161
161 def is_consistent_with(self, other):
162 """Returns True iff |other| can be used in place of |self| while keeping
163 the dependency graph consistent. Most often it means being pinned at the
164 same revision, but some specs (like path-based) are always compatible."""
165 raise NotImplementedError()
166
167 def __eq__(self, other): 162 def __eq__(self, other):
168 raise NotImplementedError() 163 raise NotImplementedError()
169 164
170 def __ne__(self, other): 165 def __ne__(self, other):
171 return not (self == other) 166 return not (self == other)
172 167
173 def proto_file(self, context): 168 def proto_file(self, context):
174 """Returns the ProtoFile of the recipes config file in this repository. 169 """Returns the ProtoFile of the recipes config file in this repository.
175 Requires a good checkout.""" 170 Requires a good checkout."""
176 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.repo_root(context))) 171 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.repo_root(context)))
(...skipping 24 matching lines...) Expand all
201 196
202 def checkout(self, context): 197 def checkout(self, context):
203 checkout_dir = self._dep_dir(context) 198 checkout_dir = self._dep_dir(context)
204 self.backend.checkout( 199 self.backend.checkout(
205 self.repo, self.revision, checkout_dir, context.allow_fetch) 200 self.repo, self.revision, checkout_dir, context.allow_fetch)
206 cleanup_pyc(checkout_dir) 201 cleanup_pyc(checkout_dir)
207 202
208 def repo_root(self, context): 203 def repo_root(self, context):
209 return os.path.join(self._dep_dir(context), self.path) 204 return os.path.join(self._dep_dir(context), self.path)
210 205
211 def is_consistent_with(self, other):
212 return (self == other)
213
214 def dump(self): 206 def dump(self):
215 buf = package_pb2.DepSpec( 207 buf = package_pb2.DepSpec(
216 project_id=self.project_id, 208 project_id=self.project_id,
217 url=self.repo, 209 url=self.repo,
218 branch=self.branch, 210 branch=self.branch,
219 revision=self.revision) 211 revision=self.revision)
220 if self.path: 212 if self.path:
221 buf.path_override = self.path 213 buf.path_override = self.path
222 214
223 # Only dump repo_type if it's different from default. This preserves 215 # Only dump repo_type if it's different from default. This preserves
(...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 def proto_file(self, context): 318 def proto_file(self, context):
327 """Returns the ProtoFile of the recipes config file in this repository. 319 """Returns the ProtoFile of the recipes config file in this repository.
328 Requires a good checkout.""" 320 Requires a good checkout."""
329 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.path)) 321 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.path))
330 322
331 def __eq__(self, other): 323 def __eq__(self, other):
332 if not isinstance(other, type(self)): 324 if not isinstance(other, type(self)):
333 return False 325 return False
334 return self.path == other.path 326 return self.path == other.path
335 327
336 def is_consistent_with(self, other):
337 # PathRepoSpec is always compatible, unless it's PathRepoSpec with a
338 # different path.
339 if isinstance(other, type(self)):
340 return self.path == other.path
341 return True
342
343 328
344 class RootRepoSpec(RepoSpec): 329 class RootRepoSpec(RepoSpec):
345 def __init__(self, proto_file): 330 def __init__(self, proto_file):
346 self._proto_file = proto_file 331 self._proto_file = proto_file
347 332
348 def checkout(self, context): 333 def checkout(self, context):
349 # We assume this is already checked out. 334 # We assume this is already checked out.
350 pass 335 pass
351 336
352 def repo_root(self, context): 337 def repo_root(self, context):
353 return context.repo_root 338 return context.repo_root
354 339
355 def is_consistent_with(self, other):
356 return (self == other)
357
358 def proto_file(self, context): 340 def proto_file(self, context):
359 return self._proto_file 341 return self._proto_file
360 342
361 def __eq__(self, other): 343 def __eq__(self, other):
362 if not isinstance(other, type(self)): 344 if not isinstance(other, type(self)):
363 return False 345 return False
364 return self._proto_file == other._proto_file 346 return self._proto_file == other._proto_file
365 347
366 348
367 class Package(object): 349 class Package(object):
368 """Package represents a loaded package, and contains path and dependency 350 """Package represents a loaded package, and contains path and dependency
369 information. 351 information.
370 352
371 This is accessed by loader.py through RecipeDeps.get_package. 353 This is accessed by loader.py through RecipeDeps.get_package.
372 """ 354 """
373 def __init__(self, name, repo_spec, deps, repo_root, relative_recipes_dir, 355 def __init__(self, name, repo_spec, deps, repo_root, relative_recipes_dir):
374 is_override):
375 self.name = name 356 self.name = name
376 self.repo_spec = repo_spec 357 self.repo_spec = repo_spec
377 self.deps = deps 358 self.deps = deps
378 self.repo_root = repo_root 359 self.repo_root = repo_root
379 self.relative_recipes_dir = relative_recipes_dir 360 self.relative_recipes_dir = relative_recipes_dir
380 self.is_override = is_override
381 361
382 def __repr__(self): 362 def __repr__(self):
383 return ('<Package(name=%r,repo_spec=%r,deps=%r,recipes_dir=%r,' 363 return '<Package(name=%r,repo_spec=%r,deps=%r,recipes_dir=%r)>' % (
384 'override=%r)>' % (self.name, self.repo_spec, self.deps, 364 self.name, self.repo_spec, self.deps, self.recipes_dir)
385 self.recipes_dir, self.is_override))
386 365
387 @property 366 @property
388 def recipes_dir(self): 367 def recipes_dir(self):
389 return os.path.join(self.repo_root, self.relative_recipes_dir) 368 return os.path.join(self.repo_root, self.relative_recipes_dir)
390 369
391 @property 370 @property
392 def recipe_dirs(self): 371 def recipe_dirs(self):
393 return [os.path.join(self.recipes_dir, 'recipes')] 372 return [os.path.join(self.recipes_dir, 'recipes')]
394 373
395 @property 374 @property
396 def module_dirs(self): 375 def module_dirs(self):
397 return [os.path.join(self.recipes_dir, 'recipe_modules')] 376 return [os.path.join(self.recipes_dir, 'recipe_modules')]
398 377
399 def find_dep(self, dep_name): 378 def find_dep(self, dep_name):
400 if dep_name == self.name: 379 if dep_name == self.name:
401 return self 380 return self
402 381
403 assert dep_name in self.deps, ( 382 assert dep_name in self.deps, (
404 '%s does not exist or is not declared as a dependency of %s' % ( 383 '%s does not exist or is not declared as a dependency of %s' % (
405 dep_name, self.name)) 384 dep_name, self.name))
406 return self.deps[dep_name] 385 return self.deps[dep_name]
407 386
408 def module_path(self, module_name): 387 def module_path(self, module_name):
409 return os.path.join(self.recipes_dir, 'recipe_modules', module_name) 388 return os.path.join(self.recipes_dir, 'recipe_modules', module_name)
410 389
411 def __repr__(self): 390 def __repr__(self):
412 return 'Package(%r, %r, %r, %r, %r)' % ( 391 return 'Package(%r, %r, %r, %r)' % (
413 self.name, self.repo_spec, self.deps, self.recipe_dirs, 392 self.name, self.repo_spec, self.deps, self.recipe_dirs)
414 self.is_override)
415 393
416 def __str__(self): 394 def __str__(self):
417 return 'Package %s, with dependencies %s' % (self.name, self.deps.keys()) 395 return 'Package %s, with dependencies %s' % (self.name, self.deps.keys())
418 396
419 397
420 class RollCandidate(object): 398 class RollCandidate(object):
421 """RollCandidate represents a recipe roll candidate, i.e. updates 399 """RollCandidate represents a recipe roll candidate, i.e. updates
422 to pinned revisions of recipe dependencies. 400 to pinned revisions of recipe dependencies.
423 401
424 This is mostly used by recipes.py autoroll command. 402 This is mostly used by recipes.py autoroll command.
(...skipping 18 matching lines...) Expand all
443 """Attempts to make the after-roll dependency graph consistent by rolling 421 """Attempts to make the after-roll dependency graph consistent by rolling
444 other package dependencies (changing their revisions). A consistent 422 other package dependencies (changing their revisions). A consistent
445 dependency graph means that all of the repos in the graph are pinned 423 dependency graph means that all of the repos in the graph are pinned
446 at the same revision. 424 at the same revision.
447 425
448 Returns True on success. 426 Returns True on success.
449 """ 427 """
450 while True: 428 while True:
451 try: 429 try:
452 package_deps = PackageDeps(self._context) 430 package_deps = PackageDeps(self._context)
453 package_deps._create_from_spec(root_spec, self.get_rolled_spec(), False) 431 package_deps._create_from_spec(root_spec, self.get_rolled_spec())
454 return True 432 return True
455 except InconsistentDependencyGraphError as e: 433 except InconsistentDependencyGraphError as e:
456 # Don't update the same project twice - that'd mean we have two 434 # Don't update the same project twice - that'd mean we have two
457 # conflicting updates anyway. 435 # conflicting updates anyway.
458 if e.project_id in self._updates: 436 if e.project_id in self._updates:
459 return False 437 return False
460 438
461 # Get the spec that is different from the one we already have. 439 # Get the spec that is different from the one we already have.
462 # The order in which they're returned is not guaranteed. 440 # The order in which they're returned is not guaranteed.
463 current_revision = self._package_spec.deps[e.project_id].revision 441 current_revision = self._package_spec.deps[e.project_id].revision
(...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after
601 579
602 def __ne__(self, other): 580 def __ne__(self, other):
603 return not self.__eq__(other) 581 return not self.__eq__(other)
604 582
605 583
606 class PackageDeps(object): 584 class PackageDeps(object):
607 """An object containing all the transitive dependencies of the root package. 585 """An object containing all the transitive dependencies of the root package.
608 """ 586 """
609 def __init__(self, context, overrides=None): 587 def __init__(self, context, overrides=None):
610 self._context = context 588 self._context = context
589 self._packages = {}
611 self._overrides = overrides or {} 590 self._overrides = overrides or {}
612 self._packages = {}
613 self._root_package = None 591 self._root_package = None
614 592
615 @property 593 @property
616 def root_package(self): 594 def root_package(self):
617 return self._root_package 595 return self._root_package
618 596
619 @classmethod 597 @classmethod
620 def create(cls, repo_root, proto_file, deps_path=None, allow_fetch=False, 598 def create(cls, repo_root, proto_file, deps_path=None, allow_fetch=False,
621 overrides=None): 599 overrides=None):
622 """Creates a PackageDeps object. 600 """Creates a PackageDeps object.
623 601
624 Arguments: 602 Arguments:
625 repo_root: the root of the repository containing this package. 603 repo_root: the root of the repository containing this package.
626 proto_file: a ProtoFile object corresponding to the repos recipes.cfg 604 proto_file: a ProtoFile object corresponding to the repos recipes.cfg
627 allow_fetch: whether to fetch dependencies rather than just checking for 605 allow_fetch: whether to fetch dependencies rather than just checking for
628 them. 606 them.
629 overrides: if not None, a dictionary of project overrides. Dictionary keys 607 overrides: if not None, a dictionary of project overrides. Dictionary keys
630 are the `project_id` field to override, and dictionary values 608 are the `project_id` field to override, and dictionary values
631 are the override path. 609 are the override path.
632 """ 610 """
633 context = PackageContext.from_proto_file(repo_root, proto_file, allow_fetch, 611 context = PackageContext.from_proto_file(repo_root, proto_file, allow_fetch,
634 deps_path=deps_path) 612 deps_path=deps_path)
635 613
636 if overrides: 614 if overrides:
637 overrides = {project_id: PathRepoSpec(path) 615 overrides = {project_id: PathRepoSpec(path)
638 for project_id, path in overrides.iteritems()} 616 for project_id, path in overrides.iteritems()}
639 package_deps = cls(context, overrides=overrides) 617 package_deps = cls(context, overrides=overrides)
640 618
641 # Install our overrides and their dependencies first. Each of these will be 619 package_deps._root_package = package_deps._create_package(RootRepoSpec(proto _file))
642 # marked as overriding, meaning that they will supercede equivalent packages
643 # defined in the root package without error.
644 #
645 # Package resolution is required to be consistent within the overridden
646 # packages, and required to be consistent within the non-overridden
647 # packages, but a conflict between an overridden package and a
648 # non-overridden package will prefer the overridden one.
649 package_deps._register_overrides()
650
651 # Install our root package and its dependencies.
652 package_deps._root_package = package_deps._create_package(
653 RootRepoSpec(proto_file), False)
654 620
655 return package_deps 621 return package_deps
656 622
657 def _register_overrides(self): 623 def _create_package(self, repo_spec):
658 for project_id, repo_spec in self._overrides.iteritems():
659 self._create_package(repo_spec, True)
660
661 def _create_package(self, repo_spec, overriding):
662 repo_spec.checkout(self._context) 624 repo_spec.checkout(self._context)
663 package_spec = PackageSpec.load_proto(repo_spec.proto_file(self._context)) 625 package_spec = PackageSpec.load_proto(repo_spec.proto_file(self._context))
664 return self._create_from_spec(repo_spec, package_spec, overriding) 626 return self._create_from_spec(repo_spec, package_spec)
665 627
666 def _create_from_spec(self, repo_spec, package_spec, overriding): 628 def _create_from_spec(self, repo_spec, package_spec):
667 project_id = package_spec.project_id 629 project_id = package_spec.project_id
668 630 repo_spec = self._overrides.get(project_id, repo_spec)
669 if project_id in self._packages: 631 if project_id in self._packages:
670 current = self._packages[project_id] 632 # TODO(phajdan.jr): Are exceptions the best way to report these errors?
671 if current is None: 633 # The way this is used in practice, especially inconsistent dependency
634 # graph condition, might be considered as using exceptions for control
635 # flow.
636 if self._packages[project_id] is None:
672 raise CyclicDependencyError( 637 raise CyclicDependencyError(
673 'Package %s depends on itself' % project_id) 638 'Package %s depends on itself' % project_id)
674 639 if repo_spec != self._packages[project_id].repo_spec:
675 # Only enforce package consistency within the override boundary.
676 # It's fine if either spec claims it's consistent with the other.
677 if (current.is_override == overriding and
678 not repo_spec.is_consistent_with(current.repo_spec) and
679 not current.repo_spec.is_consistent_with(repo_spec)):
680 raise InconsistentDependencyGraphError( 640 raise InconsistentDependencyGraphError(
681 project_id, (repo_spec, current.repo_spec)) 641 project_id, (repo_spec, self._packages[project_id].repo_spec))
682 self._packages[project_id] = None 642 self._packages[project_id] = None
683 643
684 deps = {} 644 deps = {}
685 for dep, dep_repo in sorted(package_spec.deps.items()): 645 for dep, dep_repo in sorted(package_spec.deps.items()):
686 dep_package = self._packages.get(dep) 646 dep_repo = self._overrides.get(dep, dep_repo)
687 if not (dep_package and dep_package.is_override): 647 deps[dep] = self._create_package(dep_repo)
688 dep_package = self._create_package(dep_repo, overriding)
689 deps[dep] = dep_package
690 648
691 package = Package( 649 package = Package(
692 project_id, repo_spec, deps, 650 project_id, repo_spec, deps,
693 repo_spec.repo_root(self._context), 651 repo_spec.repo_root(self._context),
694 package_spec.recipes_path, 652 package_spec.recipes_path)
695 overriding)
696 653
697 self._packages[project_id] = package 654 self._packages[project_id] = package
698 return package 655 return package
699 656
700 # TODO(luqui): Remove this, so all accesses to packages are done 657 # TODO(luqui): Remove this, so all accesses to packages are done
701 # via other packages with properly scoped deps. 658 # via other packages with properly scoped deps.
702 def get_package(self, package_id): 659 def get_package(self, package_id):
703 return self._packages[package_id] 660 return self._packages[package_id]
704 661
705 @property 662 @property
(...skipping 12 matching lines...) Expand all
718 >>> d = { 'x': 1, 'y': 2 } 675 >>> d = { 'x': 1, 'y': 2 }
719 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items()) 676 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items())
720 [('x', 1), ('y', 3), ('z', 4)] 677 [('x', 1), ('y', 3), ('z', 4)]
721 >>> sorted(d.items()) 678 >>> sorted(d.items())
722 [('x', 1), ('y', 2)] 679 [('x', 1), ('y', 2)]
723 """ 680 """
724 681
725 d = copy.copy(d) 682 d = copy.copy(d)
726 d.update(updates) 683 d.update(updates)
727 return d 684 return d
OLDNEW
« no previous file with comments | « no previous file | recipes.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698