Added unit tests for spec normalization.

This commit is contained in:
Todd Gamblin 2013-10-18 16:39:09 -07:00
parent 481a617d65
commit 7bdf93234a
3 changed files with 135 additions and 25 deletions

View File

@ -380,7 +380,8 @@ def preorder_traversal(self, visited=None):
visited.add(self.name) visited.add(self.name)
yield self yield self
for name, spec in self.dependencies.iteritems(): for name in sorted(self.dependencies.keys()):
spec = self.dependencies[name]
for pkg in packages.get(name).preorder_traversal(visited): for pkg in packages.get(name).preorder_traversal(visited):
yield pkg yield pkg

View File

@ -78,6 +78,7 @@
from spack.util.lang import * from spack.util.lang import *
from spack.util.string import * from spack.util.string import *
"""This map determines the coloring of specs when using color output. """This map determines the coloring of specs when using color output.
We make the fields different colors to enhance readability. We make the fields different colors to enhance readability.
See spack.color for descriptions of the color codes. See spack.color for descriptions of the color codes.
@ -226,7 +227,7 @@ def __str__(self):
@key_ordering @key_ordering
class Spec(object): class Spec(object):
def __init__(self, spec_like): def __init__(self, spec_like, *dep_like):
# Copy if spec_like is a Spec. # Copy if spec_like is a Spec.
if type(spec_like) == Spec: if type(spec_like) == Spec:
self._dup(spec_like) self._dup(spec_like)
@ -254,6 +255,16 @@ def __init__(self, spec_like):
self.compiler = other.compiler self.compiler = other.compiler
self.dependencies = other.dependencies self.dependencies = other.dependencies
# This allows users to construct a spec DAG with literals.
# Note that given two specs a and b, Spec(a) copies a, but
# Spec(a, b) will copy a but just add b as a dep.
for dep in dep_like:
if type(dep) == str:
dep_spec = Spec(dep)
self.dependencies[dep_spec.name] = dep_spec
elif type(dep) == Spec:
self.dependencies[dep.name] = dep
# #
# Private routines here are called by the parser when building a spec. # Private routines here are called by the parser when building a spec.
@ -315,17 +326,27 @@ def concrete(self):
and self.dependencies.concrete) and self.dependencies.concrete)
def preorder_traversal(self, visited=None): def preorder_traversal(self, visited=None, d=0, **kwargs):
unique = kwargs.setdefault('unique', True)
depth = kwargs.setdefault('depth', False)
keyfun = kwargs.setdefault('key', id)
noroot = kwargs.setdefault('noroot', False)
if visited is None: if visited is None:
visited = set() visited = set()
if id(self) in visited: if keyfun(self) in visited:
if not unique:
yield (d, self) if depth else self
return return
visited.add(id(self)) visited.add(keyfun(self))
yield self if d > 0 or not noroot:
for dep in self.dependencies.itervalues(): yield (d, self) if depth else self
for spec in dep.preorder_traversal(visited):
for key in sorted(self.dependencies.keys()):
for spec in self.dependencies[key].preorder_traversal(
visited, d+1, **kwargs):
yield spec yield spec
@ -368,10 +389,14 @@ def _concretize(self):
# Ensure dependencies have right versions # Ensure dependencies have right versions
def flatten(self): def flat_dependencies(self):
"""Pull all dependencies up to the root (this spec). """Return a DependencyMap containing all dependencies with their
Merge constraints for dependencies with the same name, and if they constraints merged. If there are any conflicts, throw an exception.
conflict, throw an exception. """
This will work even on specs that are not normalized; i.e. specs
that have two instances of the same dependency in the DAG.
This is used as the first step of normalization.
"""
# This ensures that the package descriptions themselves are consistent # This ensures that the package descriptions themselves are consistent
self.package.validate_dependencies() self.package.validate_dependencies()
@ -393,7 +418,14 @@ def flatten(self):
# so this means OUR code is not sane! # so this means OUR code is not sane!
raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message) raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message)
self.dependencies = flat_deps return flat_deps
def flatten(self):
"""Pull all dependencies up to the root (this spec).
Merge constraints for dependencies with the same name, and if they
conflict, throw an exception. """
self.dependencies = self.flat_dependencies()
def _normalize_helper(self, visited, spec_deps): def _normalize_helper(self, visited, spec_deps):
@ -432,11 +464,8 @@ def normalize(self):
# Then ensure that the packages mentioned are sane, that the # Then ensure that the packages mentioned are sane, that the
# provided spec is sane, and that all dependency specs are in the # provided spec is sane, and that all dependency specs are in the
# root node of the spec. Flatten will do this for us. # root node of the spec. flat_dependencies will do this for us.
self.flatten() spec_deps = self.flat_dependencies()
# Now that we're flat we can get all our dependencies at once.
spec_deps = self.dependencies
self.dependencies = DependencyMap() self.dependencies = DependencyMap()
visited = set() visited = set()
@ -535,7 +564,7 @@ def colorized(self):
return colorize_spec(self) return colorize_spec(self)
def str_without_deps(self): def str_no_deps(self):
out = self.name out = self.name
# If the version range is entirely open, omit it # If the version range is entirely open, omit it
@ -553,12 +582,19 @@ def str_without_deps(self):
return out return out
def tree(self, indent=""): def tree(self):
"""Prints out this spec and its dependencies, tree-formatted """Prints out this spec and its dependencies, tree-formatted
with indentation. Each node also has an id.""" with indentation."""
out = indent + self.str_without_deps() out = ""
for dep in sorted(self.dependencies.keys()): cur_id = 0
out += "\n" + self.dependencies[dep].tree(indent + " ") ids = {}
for d, node in self.preorder_traversal(unique=False, depth=True):
if not id(node) in ids:
cur_id += 1
ids[id(node)] = cur_id
out += str(ids[id(node)])
out += " "+ (" " * d)
out += node.str_no_deps() + "\n"
return out return out
@ -567,7 +603,11 @@ def __repr__(self):
def __str__(self): def __str__(self):
return self.str_without_deps() + str(self.dependencies) byname = lambda d: d.name
deps = self.preorder_traversal(key=byname, noroot=True)
sorted_deps = sorted(deps, key=byname)
dep_string = ''.join("^" + dep.str_no_deps() for dep in sorted_deps)
return self.str_no_deps() + dep_string
# #

View File

@ -115,3 +115,72 @@ def test_unsatisfiable_architecture(self):
set_pkg_dep('mpileaks', 'mpich=bgqos_0') set_pkg_dep('mpileaks', 'mpich=bgqos_0')
spec = Spec('mpileaks ^mpich=sles_10_ppc64 ^callpath ^dyninst ^libelf ^libdwarf') spec = Spec('mpileaks ^mpich=sles_10_ppc64 ^callpath ^dyninst ^libelf ^libdwarf')
self.assertRaises(spack.spec.UnsatisfiableArchitectureSpecError, spec.normalize) self.assertRaises(spack.spec.UnsatisfiableArchitectureSpecError, spec.normalize)
def test_invalid_dep(self):
spec = Spec('libelf ^mpich')
self.assertRaises(spack.spec.InvalidDependencyException, spec.normalize)
spec = Spec('libelf ^libdwarf')
self.assertRaises(spack.spec.InvalidDependencyException, spec.normalize)
spec = Spec('mpich ^dyninst ^libelf')
self.assertRaises(spack.spec.InvalidDependencyException, spec.normalize)
def test_equal(self):
spec = Spec('mpileaks ^callpath ^libelf ^libdwarf')
self.assertNotEqual(spec, Spec(
'mpileaks', Spec('callpath',
Spec('libdwarf',
Spec('libelf')))))
self.assertNotEqual(spec, Spec(
'mpileaks', Spec('callpath',
Spec('libelf',
Spec('libdwarf')))))
self.assertEqual(spec, Spec(
'mpileaks', Spec('callpath'), Spec('libdwarf'), Spec('libelf')))
self.assertEqual(spec, Spec(
'mpileaks', Spec('libelf'), Spec('libdwarf'), Spec('callpath')))
def test_normalize_mpileaks(self):
spec = Spec('mpileaks ^mpich ^callpath ^dyninst ^libelf@1.8.11 ^libdwarf')
expected_flat = Spec(
'mpileaks', Spec('mpich'), Spec('callpath'), Spec('dyninst'),
Spec('libelf@1.8.11'), Spec('libdwarf'))
mpich = Spec('mpich')
libelf = Spec('libelf@1.8.11')
expected_normalized = Spec(
'mpileaks',
Spec('callpath',
Spec('dyninst', Spec('libdwarf', libelf),
libelf),
mpich), mpich)
expected_non_dag = Spec(
'mpileaks',
Spec('callpath',
Spec('dyninst', Spec('libdwarf', Spec('libelf@1.8.11')),
Spec('libelf@1.8.11')),
mpich), Spec('mpich'))
self.assertEqual(expected_normalized, expected_non_dag)
self.assertEqual(str(expected_normalized), str(expected_non_dag))
self.assertEqual(str(spec), str(expected_non_dag))
self.assertEqual(str(expected_normalized), str(spec))
self.assertEqual(spec, expected_flat)
self.assertNotEqual(spec, expected_normalized)
self.assertNotEqual(spec, expected_non_dag)
spec.normalize()
self.assertNotEqual(spec, expected_flat)
self.assertEqual(spec, expected_normalized)
self.assertEqual(spec, expected_non_dag)