graph: refactor static graphs

- `spack graph --static` (and `spack.graph.dot_graph`) now do the "right
  thing" and print the possible dependency graph of provided packages.

- `spack graph --static` no longer concretizes specs, as it only relies
  on class level metadata

- Previously the behavior was not consistent -- `spack graph --static`
  would graph possible dependencies of concrete specs, but would only
  include some of them.  The new code properly pursues all possible
  dependencies, and allows traversing by different dependency types.
This commit is contained in:
Todd Gamblin 2019-04-20 22:44:40 -07:00
parent 2e22fc1090
commit dc8af3023e
4 changed files with 73 additions and 100 deletions

View file

@ -9,9 +9,9 @@
import llnl.util.tty as tty
import spack.cmd
import spack.cmd.common.arguments as arguments
import spack.config
import spack.store
from spack.dependency import all_deptypes, canonical_deptype
from spack.graph import graph_dot, graph_ascii
description = "generate graphs of package dependency relationships"
@ -30,22 +30,15 @@ def setup_parser(subparser):
'-d', '--dot', action='store_true',
help="generate graph in dot format and print to stdout")
subparser.add_argument(
'-n', '--normalize', action='store_true',
help="skip concretization; only print normalized spec")
subparser.add_argument(
'-s', '--static', action='store_true',
help="use static information from packages, not dynamic spec info")
help="graph static (possible) deps, don't concretize (implies --dot)")
subparser.add_argument(
'-i', '--installed', action='store_true',
help="graph all installed specs in dot format (implies --dot)")
subparser.add_argument(
'-t', '--deptype', action='store',
help="comma-separated list of deptypes to traverse. default=%s"
% ','.join(all_deptypes))
arguments.add_common_arguments(subparser, ['deptype'])
subparser.add_argument(
'specs', nargs=argparse.REMAINDER,
@ -53,7 +46,6 @@ def setup_parser(subparser):
def graph(parser, args):
concretize = not args.normalize
if args.installed:
if args.specs:
tty.die("Can't specify specs with --installed")
@ -61,26 +53,21 @@ def graph(parser, args):
specs = spack.store.db.query()
else:
specs = spack.cmd.parse_specs(
args.specs, normalize=True, concretize=concretize)
specs = spack.cmd.parse_specs(args.specs, concretize=not args.static)
if not specs:
setup_parser.parser.print_help()
return 1
deptype = all_deptypes
if args.deptype:
deptype = tuple(args.deptype.split(','))
if deptype == ('all',):
deptype = 'all'
deptype = canonical_deptype(deptype)
if args.static:
args.dot = True
if args.dot: # Dot graph only if asked for.
graph_dot(specs, static=args.static, deptype=deptype)
if args.dot:
graph_dot(specs, static=args.static, deptype=args.deptype)
elif specs: # ascii is default: user doesn't need to provide it explicitly
debug = spack.config.get('config:debug')
graph_ascii(specs[0], debug=debug, deptype=deptype)
graph_ascii(specs[0], debug=debug, deptype=args.deptype)
for spec in specs[1:]:
print() # extra line bt/w independent graphs
graph_ascii(spec, debug=debug)

View file

@ -43,13 +43,10 @@
"""
import sys
from heapq import heapify, heappop, heappush
from six import iteritems
from llnl.util.tty.color import ColorStream
from spack.spec import Spec
from spack.dependency import all_deptypes, canonical_deptype
@ -499,76 +496,67 @@ def graph_dot(specs, deptype='all', static=False, out=None):
spack graph --dot qt | dot -Tpdf > spack-graph.pdf
"""
if not specs:
raise ValueError("Must provide specs to graph_dot")
if out is None:
out = sys.stdout
deptype = canonical_deptype(deptype)
def static_graph(spec, deptype):
pkg = spec.package
possible = pkg.possible_dependencies(
expand_virtuals=True, deptype=deptype)
nodes = set() # elements are (node name, node label)
edges = set() # elements are (src key, dest key)
for name, deps in possible.items():
nodes.add((name, name))
edges.update((name, d) for d in deps)
return nodes, edges
def dynamic_graph(spec, deptypes):
nodes = set() # elements are (node key, node label)
edges = set() # elements are (src key, dest key)
for s in spec.traverse(deptype=deptype):
nodes.add((s.dag_hash(), s.name))
for d in s.dependencies(deptype=deptype):
edge = (s.dag_hash(), d.dag_hash())
edges.add(edge)
return nodes, edges
nodes = set()
edges = set()
for spec in specs:
if static:
n, e = static_graph(spec, deptype)
else:
n, e = dynamic_graph(spec, deptype)
nodes.update(n)
edges.update(e)
out.write('digraph G {\n')
out.write(' labelloc = "b"\n')
out.write(' rankdir = "TB"\n')
out.write(' ranksep = "5"\n')
out.write('node[\n')
out.write(' ranksep = "1"\n')
out.write(' edge[\n')
out.write(' penwidth=4')
out.write(' ]\n')
out.write(' node[\n')
out.write(' fontname=Monaco,\n')
out.write(' penwidth=2,\n')
out.write(' fontsize=12,\n')
out.write(' margin=.1,\n')
out.write(' penwidth=4,\n')
out.write(' fontsize=24,\n')
out.write(' margin=.2,\n')
out.write(' shape=box,\n')
out.write(' fillcolor=lightblue,\n')
out.write(' style="rounded,filled"]\n')
out.write(' style="rounded,filled"')
out.write(' ]\n')
out.write('\n')
def q(string):
return '"%s"' % string
if not specs:
raise ValueError("Must provide specs ot graph_dot")
# Static graph includes anything a package COULD depend on.
if static:
names = set.union(*[
s.package.possible_dependencies(expand_virtuals=False)
for s in specs])
specs = [Spec(name) for name in names]
labeled = set()
def label(key, label):
if key not in labeled:
out.write(' "%s" [label="%s"]\n' % (key, label))
labeled.add(key)
deps = set()
for spec in specs:
if static:
out.write(' "%s" [label="%s"]\n' % (spec.name, spec.name))
# Skip virtual specs (we'll find out about them from concrete ones.
if spec.virtual:
continue
# Add edges for each depends_on in the package.
for dep_name, dep in iteritems(spec.package.dependencies):
deps.add((spec.name, dep_name))
# If the package provides something, add an edge for that.
for provider in set(s.name for s in spec.package.provided):
deps.add((provider, spec.name))
else:
def key_label(s):
return s.dag_hash(), "%s/%s" % (s.name, s.dag_hash(7))
for s in spec.traverse(deptype=deptype):
skey, slabel = key_label(s)
out.write(' "%s" [label="%s"]\n' % (skey, slabel))
for d in s.dependencies(deptype=deptype):
dkey, _ = key_label(d)
deps.add((skey, dkey))
for key, label in nodes:
out.write(' "%s" [label="%s"]\n' % (key, label))
out.write('\n')
for pair in deps:
out.write(' "%s" -> "%s"\n' % pair)
for src, dest in edges:
out.write(' "%s" -> "%s"\n' % (src, dest))
out.write('}\n')

View file

@ -25,13 +25,6 @@ def test_graph_dot():
graph('--dot', 'dt-diamond')
@pytest.mark.db
@pytest.mark.usefixtures('mock_packages', 'database')
def test_graph_normalize():
"""Tests spack graph --normalize"""
graph('--normalize', 'dt-diamond')
@pytest.mark.db
@pytest.mark.usefixtures('mock_packages', 'database')
def test_graph_static():

View file

@ -5,6 +5,7 @@
from six import StringIO
import spack.repo
from spack.spec import Spec
from spack.graph import AsciiGraph, topological_sort, graph_dot
@ -47,34 +48,38 @@ def test_static_graph_mpileaks(mock_packages):
assert ' "libelf" [label="libelf"]\n' in dot
assert ' "libdwarf" [label="libdwarf"]\n' in dot
mpi_providers = spack.repo.path.providers_for('mpi')
for spec in mpi_providers:
assert ('"mpileaks" -> "%s"' % spec.name) in dot
assert ('"callpath" -> "%s"' % spec.name) in dot
assert ' "dyninst" -> "libdwarf"\n' in dot
assert ' "callpath" -> "dyninst"\n' in dot
assert ' "mpileaks" -> "mpi"\n' in dot
assert ' "libdwarf" -> "libelf"\n' in dot
assert ' "callpath" -> "mpi"\n' in dot
assert ' "mpileaks" -> "callpath"\n' in dot
assert ' "dyninst" -> "libelf"\n' in dot
def test_dynamic_dot_graph_mpileaks(mock_packages):
def test_dynamic_dot_graph_mpileaks(mock_packages, config):
"""Test dynamically graphing the mpileaks package."""
s = Spec('mpileaks').normalized()
s = Spec('mpileaks').concretized()
stream = StringIO()
graph_dot([s], static=False, out=stream)
dot = stream.getvalue()
print(dot)
mpileaks_hash, mpileaks_lbl = s.dag_hash(), s.format('{name}{/hash:7}')
mpi_hash, mpi_lbl = s['mpi'].dag_hash(), s['mpi'].format('{name}{/hash:7}')
mpileaks_hash, mpileaks_lbl = s.dag_hash(), s.format('{name}')
mpi_hash, mpi_lbl = s['mpi'].dag_hash(), s['mpi'].format('{name}')
callpath_hash, callpath_lbl = (
s['callpath'].dag_hash(), s['callpath'].format('{name}{/hash:7}'))
s['callpath'].dag_hash(), s['callpath'].format('{name}'))
dyninst_hash, dyninst_lbl = (
s['dyninst'].dag_hash(), s['dyninst'].format('{name}{/hash:7}'))
s['dyninst'].dag_hash(), s['dyninst'].format('{name}'))
libdwarf_hash, libdwarf_lbl = (
s['libdwarf'].dag_hash(), s['libdwarf'].format('{name}{/hash:7}'))
s['libdwarf'].dag_hash(), s['libdwarf'].format('{name}'))
libelf_hash, libelf_lbl = (
s['libelf'].dag_hash(), s['libelf'].format('{name}{/hash:7}'))
s['libelf'].dag_hash(), s['libelf'].format('{name}'))
assert ' "%s" [label="%s"]\n' % (mpileaks_hash, mpileaks_lbl) in dot
assert ' "%s" [label="%s"]\n' % (callpath_hash, callpath_lbl) in dot