more-flexible user-specified dependency constraints (#8162)

* allow user to constrain dependencies that are added conditionally

* remove check for not-visited deps from normalize, move it to concretize. The check now runs after the concretization loop completes (so an error is only reported if the user-mentioned spec doesnt appear anywhere in the dag)

* remove separate full_spec_deps variable; rename spec_deps to all_spec_deps to clarify that it merges user-specified dependencies with derived dependencies

* add unit test to confirm new functionality
This commit is contained in:
scheibelp 2018-05-30 11:07:13 -07:00 committed by Todd Gamblin
parent 55e42dca2c
commit 43114c2e06
3 changed files with 80 additions and 17 deletions

View File

@ -1833,13 +1833,26 @@ def concretize(self, tests=False):
changed = True changed = True
force = False force = False
user_spec_deps = self.flat_dependencies(copy=False)
while changed: while changed:
changes = (self.normalize(force, tests), changes = (self.normalize(force, tests=tests,
user_spec_deps=user_spec_deps),
self._expand_virtual_packages(), self._expand_virtual_packages(),
self._concretize_helper()) self._concretize_helper())
changed = any(changes) changed = any(changes)
force = True force = True
visited_user_specs = set()
for dep in self.traverse():
visited_user_specs.add(dep.name)
visited_user_specs.update(x.name for x in dep.package.provided)
extra = set(user_spec_deps.keys()).difference(visited_user_specs)
if extra:
raise InvalidDependencyError(
self.name + " does not depend on " + comma_or(extra))
for s in self.traverse(): for s in self.traverse():
# After concretizing, assign namespaces to anything left. # After concretizing, assign namespaces to anything left.
# Note that this doesn't count as a "change". The repository # Note that this doesn't count as a "change". The repository
@ -2190,7 +2203,7 @@ def _normalize_helper(self, visited, spec_deps, provider_index, tests):
return any_change return any_change
def normalize(self, force=False, tests=False): def normalize(self, force=False, tests=False, user_spec_deps=None):
"""When specs are parsed, any dependencies specified are hanging off """When specs are parsed, any dependencies specified are hanging off
the root, and ONLY the ones that were explicitly provided are there. the root, and ONLY the ones that were explicitly provided are there.
Normalization turns a partial flat spec into a DAG, where: Normalization turns a partial flat spec into a DAG, where:
@ -2219,27 +2232,34 @@ def normalize(self, force=False, tests=False):
# Ensure first that all packages & compilers in the DAG exist. # Ensure first that all packages & compilers in the DAG exist.
self.validate_or_raise() self.validate_or_raise()
# Get all the dependencies into one DependencyMap # Clear the DAG and collect all dependencies in the DAG, which will be
spec_deps = self.flat_dependencies(copy=False) # reapplied as constraints. All dependencies collected this way will
# have been created by a previous execution of 'normalize'.
# A dependency extracted here will only be reintegrated if it is
# discovered to apply according to _normalize_helper, so
# user-specified dependencies are recorded separately in case they
# refer to specs which take several normalization passes to
# materialize.
all_spec_deps = self.flat_dependencies(copy=False)
if user_spec_deps:
for name, spec in user_spec_deps.items():
if name not in all_spec_deps:
all_spec_deps[name] = spec
else:
all_spec_deps[name].constrain(spec)
# Initialize index of virtual dependency providers if # Initialize index of virtual dependency providers if
# concretize didn't pass us one already # concretize didn't pass us one already
provider_index = ProviderIndex( provider_index = ProviderIndex(
[s for s in spec_deps.values()], restrict=True) [s for s in all_spec_deps.values()], restrict=True)
# traverse the package DAG and fill out dependencies according # traverse the package DAG and fill out dependencies according
# to package files & their 'when' specs # to package files & their 'when' specs
visited = set() visited = set()
any_change = self._normalize_helper( any_change = self._normalize_helper(
visited, spec_deps, provider_index, tests) visited, all_spec_deps, provider_index, tests)
# If there are deps specified but not visited, they're not
# actually deps of this package. Raise an error.
extra = set(spec_deps.keys()).difference(visited)
if extra:
raise InvalidDependencyError(
self.name + " does not depend on " + comma_or(extra))
# Mark the spec as normal once done. # Mark the spec as normal once done.
self._normal = True self._normal = True

View File

@ -611,7 +611,11 @@ def __init__(self, name, dependencies, dependency_types, conditions=None,
if not conditions or dep.name not in conditions: if not conditions or dep.name not in conditions:
self.dependencies[dep.name] = {Spec(name): d} self.dependencies[dep.name] = {Spec(name): d}
else: else:
self.dependencies[dep.name] = {Spec(conditions[dep.name]): d} dep_conditions = conditions[dep.name]
dep_conditions = dict(
(Spec(x), Dependency(self, Spec(y), type=dtype))
for x, y in dep_conditions.items())
self.dependencies[dep.name] = dep_conditions
if versions: if versions:
self.versions = versions self.versions = versions

View File

@ -102,6 +102,44 @@ def test_test_deptype():
assert ('z' not in spec) assert ('z' not in spec)
@pytest.mark.usefixtures('config')
def test_conditional_dep_with_user_constraints():
"""This sets up packages X->Y such that X depends on Y conditionally. It
then constructs a Spec with X but with no constraints on X, so that the
initial normalization pass cannot determine whether the constraints are
met to add the dependency; this checks whether a user-specified constraint
on Y is applied properly.
"""
default = ('build', 'link')
y = MockPackage('y', [], [])
x_on_y_conditions = {
y.name: {
'x@2:': 'y'
}
}
x = MockPackage('x', [y], [default], conditions=x_on_y_conditions)
mock_repo = MockPackageMultiRepo([x, y])
with spack.repo.swap(mock_repo):
spec = Spec('x ^y@2')
spec.concretize()
assert ('y@2' in spec)
with spack.repo.swap(mock_repo):
spec = Spec('x@1')
spec.concretize()
assert ('y' not in spec)
with spack.repo.swap(mock_repo):
spec = Spec('x')
spec.concretize()
assert ('y@3' in spec)
@pytest.mark.usefixtures('mutable_mock_packages') @pytest.mark.usefixtures('mutable_mock_packages')
class TestSpecDag(object): class TestSpecDag(object):
@ -300,18 +338,19 @@ def test_unsatisfiable_architecture(self, set_dependency):
with pytest.raises(spack.spec.UnsatisfiableArchitectureSpecError): with pytest.raises(spack.spec.UnsatisfiableArchitectureSpecError):
spec.normalize() spec.normalize()
@pytest.mark.usefixtures('config')
def test_invalid_dep(self): def test_invalid_dep(self):
spec = Spec('libelf ^mpich') spec = Spec('libelf ^mpich')
with pytest.raises(spack.spec.InvalidDependencyError): with pytest.raises(spack.spec.InvalidDependencyError):
spec.normalize() spec.concretize()
spec = Spec('libelf ^libdwarf') spec = Spec('libelf ^libdwarf')
with pytest.raises(spack.spec.InvalidDependencyError): with pytest.raises(spack.spec.InvalidDependencyError):
spec.normalize() spec.concretize()
spec = Spec('mpich ^dyninst ^libelf') spec = Spec('mpich ^dyninst ^libelf')
with pytest.raises(spack.spec.InvalidDependencyError): with pytest.raises(spack.spec.InvalidDependencyError):
spec.normalize() spec.concretize()
def test_equal(self): def test_equal(self):
# Different spec structures to test for equality # Different spec structures to test for equality