Repository: spack/spack

Issue:
spack spec -t can report misleading dependency types in case of multiple dependencies
We noticed this internally with one of our more complicated meta packages. But it is straight forward to reproduce by creating 4 packages:
* a: depends on b and c with either link or run dependency
* b: depends on d type build
* c: depends on d type either link or run
* d: no dependencies

### Expected Result

```
spack spec -t a
```
to correctly identify d being a link/run dependency instead of only being a build dependency.

### Actual Result

```
$ spack spec -t a
Input spec
--------------------------------
[    ]  a

Concretized
--------------------------------
[    ]  a@1%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[bl  ]      ^b@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[b   ]          ^d@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[bl  ]      ^c@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
```


I'll try to track this down either later today or this week, but for now we should tread the type identification of dependencies of dependencies from `spack spec -t` as unreliable.


Discussion:
Okay now my confusion only increases. I basically stepped through the `spec.concretize()` part of the parser. And as far as I can see the `link` dependency of `c` on `d` is simply gone after the concretisation finishes.

I'm confused, because our `view` generation correctly adds `d` to the view if there is a `link` or `run` type dependency on it anywhere in the dag. But it doesn't when there are only `build` dependencies.

I believe I tracked it to `spec.py:l2172` 
```
    def _normalize_helper(self, visited, spec_deps, provider_index, tests):
        """Recursive helper function for _normalize."""
        if self.name in visited:
            return False
```
this `return False` is only the right thing to do if the dependency types agree other wise the dependency types need to be updated, unfortunately I have no idea how to do this (and neither what the semantics of the output should be then)
In this case a correct DAG would be
```
[    ]  a@1%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[bl  ]      ^b@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[bl  ]      ^c@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
[bl  ]          ^d@1.2.3%clang@10.0.0-apple arch=darwin-highsierra-x86_64 
```
but this is only true as long there is no `depends_on('xyz', type='build')` and `depends_on('xyz', type='link')`in packages `xx` and `yy` respectively. Then placing the union of dependencies under one would be ?misleading?

As I'm needing help for this pinging people I think might know what to do @scheibelp @tgamblin 

edit: Found out why our view generation does the right thing ™️. It is really because `spack view` normally does the WRONG thing... In `filesystem_view.py:add_specs` (line 192) if `with_dependencies` is `True` `filesystem_view.py :get_dependencies` (line 570) gets called and that simply takes everything that is in the DAG (rather than everything that is not a `build`-only dependency).
In principle `view` should rely on the `Spec` object for figuring out what the relevant dependencies are, but I fear this is at least potentially a source for rather subtle bugs...
@healther: you're just seeing an artifact of how `spack spec` renders the DAG by default.  `spack spec` is a tree view of a DAG.  It's intended to make the view more manageable.  By default we only show each node in the DAG once, because if we traverse every edge, the output can get very large.  Consider, for example, that `spack spec` output can *already* get large for things that have lots of dependencies.

If you ask for `-t`, which shows dependency types, you'll only see the dependency types *on the first edge traversed* to a node.  If the traversal encounters the node again, we skip it because it's already been printed.

In your example, the dependency graph looks like this:

```
$ spack graph a
o  a
|\
o |  c
| o  b
|/
o  d
```

There are two edges into `d` and you're only seeing the one from `c` to `d` because it's the first one traversed.

If you use `--cover edges`, you'll see the dependency type on the edge from `b` to `d` as well, and you'll see that it is correct (my graph has a `link` and a `run` dependency to show that both work -- yours just has `link`):

```
$ spack spec -t --cover edges a
[    ]  a@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
[ lr ]      ^b@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
[b   ]          ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
[ lr ]      ^c@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
[ lr ]          ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
```

`--cover nodes` is the default and guarantees only that each node is shown once.  `--cover edges` guarantees that each *edge* is displayed once, so it shows both the `b -> d` and `c -> d`.

For completeness, there is also `--cover paths`, which will covers unique paths in the graph.  for the above graph, `--cover paths` output is the same as for `--cover edges`, but now suppose that `b` also depends on `c`:

```
$ spack graph a
o  a
|\
| o  b
|/| 
o |  c
|/
o  d
```

Now, `--cover edges` shows this:

```
 spack spec -t --cover edges a
[    ]  a@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]      ^b@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]          ^c@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]              ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[b   ]          ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]      ^c@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
```

It gets to `c` the first time through `a -> b -> c`, but it doesn't descend past `c` the second time, when it is reached through `a -> c`, as it has already covered the `c -> d` edge.  If you do `--cover paths`, it will *always* descend into sub-graphs even if they've already been covered by another path:

```
$ spack spec -t --cover paths a
[    ]  a@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]      ^b@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]          ^c@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]              ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[b   ]          ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]      ^c@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64 
[ lr ]          ^d@1.0%clang@8.1.0-apple arch=darwin-highsierra-x86_64
```

The advantage of that is that every time a node appears, all of its dependencies are shown.  The obvious disadvantage is that the output can get really long for even moderately complex graphs.

Does that help?
@healther: The view bug you found should probably be a separate issue -- as it might matter in other situations.  Can you add that?
Mh okay, this seems reasonable, let check what `view` is then actually doing. I.e. whether `s.traverse()` disregards `build`-only dependencies or not.

I'm not sure if the output from `spack spec` as it is right now is the most useful, what (at least) I am interested when I'm going to `spack spec` is: "What are the dependencies of my partial spec?" I.e. I'm less interested whether `a` depends on `d` through `b` or through `c` than I'm interested in in which ways `a` ends up depending on `d`. I.e. Is it necessary for me to keep `d` around if I want to use `a`.

Would it be possible to print something like an "effective" dependency? I.e. everything `a` only depends on through a `build` or `test` dependency can be removed, but everything `a` has a `link` or `run` dependency on needs to be kept around. 
I mean, that's obviously useful.  If you want to add something like that feel free 😄.  Something merged might be better for the default `-t` view than what is currently there.
> Something merged might be better for the default -t view than what is currently there.

+1 to this. The default view for `d` would be more informative saying `[blr ]` (merging incoming edges) than just `[b   ]`. 
Okay so very strange things are happening (take what I'm about to say with a grain of salt because I take my information from a rather large DAG, that I can't easily isolate; will do a minimal view as soon as I get to it):
`s.traverse()` seems to only follow `link` and `run` edges, even though `get_dependencies` calls it without an explicit `deptype` and `traverse_edges` defaults to `deptype='all'`. Nevertheless `libtool` does not appear in the final view even though there are multiple packages that have a `depends_on('libtool', type='build')` in the requested DAG.

I'm currently even uncertain what the intended behaviour should be. Given that we only noted it because someone wanted to do something with `libtool` and hence was missing it (so having it added would have spared us the hiccup). On the other hand `build`-only dependencies should be able to be removed without affecting anything (which should be currently broken, again I need to get to check this).

Question: If `a` has a `build`-only dependency on `b` and I do `spack install a && spack uninstall b` will spack try to uninstall `a`? (If someone knows by heart otherwise I'll check this with the other stuff I need to test)

> I mean, that's obviously useful. If you want to add something like that feel free 😄. Something merged might be better for the default -t view than what is currently there.

Since I was assuming the DAG to be faulty that is what I spend a couple of hours on yesterday^^ I guess the question was more a: Can it easily be done? (I assume not)

edit: I'll reopen this issue and rename it just as a reminder to myself

Resolving patch:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -3198,7 +3198,7 @@ def tree(self, **kwargs):
         fmt = kwargs.pop('format', '$_$@$%@+$+$=')
         prefix = kwargs.pop('prefix', None)
         show_types = kwargs.pop('show_types', False)
-        deptypes = kwargs.pop('deptypes', ('build', 'link'))
+        deptypes = kwargs.pop('deptypes', 'all')
         check_kwargs(kwargs, self.tree)
 
         out = ""
@@ -3226,12 +3226,21 @@ def tree(self, **kwargs):
                 out += colorize('@K{%s}  ', color=color) % node.dag_hash(hlen)
 
             if show_types:
+                types = set()
+                if cover == 'nodes':
+                    # when only covering nodes, we merge dependency types
+                    # from all dependents before showing them.
+                    for name, ds in node.dependents_dict().items():
+                        if ds.deptypes:
+                            types.update(set(ds.deptypes))
+                elif dep_spec.deptypes:
+                    # when covering edges or paths, we show dependency
+                    # types only for the edge through which we visited
+                    types = set(dep_spec.deptypes)
+
                 out += '['
-                if dep_spec.deptypes:
-                    for t in all_deptypes:
-                        out += ''.join(t[0] if t in dep_spec.deptypes else ' ')
-                else:
-                    out += ' ' * len(all_deptypes)
+                for t in all_deptypes:
+                    out += ''.join(t[0] if t in types else ' ')
                 out += ']  '
 
             out += ("    " * d)


Test patch:
diff --git a/lib/spack/spack/test/cmd/spec.py b/lib/spack/spack/test/cmd/spec.py
--- a/lib/spack/spack/test/cmd/spec.py
+++ b/lib/spack/spack/test/cmd/spec.py
@@ -22,13 +22,18 @@
 # License along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
+import re
+import pytest
+
 import spack.spec
 from spack.main import SpackCommand
 
+pytestmark = pytest.mark.usefixtures('config', 'mutable_mock_packages')
+
 spec = SpackCommand('spec')
 
 
-def test_spec(mock_packages, config):
+def test_spec():
     output = spec('mpileaks')
 
     assert 'mpileaks@2.3' in output
@@ -39,7 +44,7 @@ def test_spec(mock_packages, config):
     assert 'mpich@3.0.4' in output
 
 
-def test_spec_yaml(mock_packages, config):
+def test_spec_yaml():
     output = spec('--yaml', 'mpileaks')
 
     mpileaks = spack.spec.Spec.from_yaml(output)
@@ -49,3 +54,37 @@ def test_spec_yaml(mock_packages, config):
     assert 'libdwarf' in mpileaks
     assert 'libelf' in mpileaks
     assert 'mpich' in mpileaks
+
+
+def _parse_types(string):
+    """Parse deptypes for specs from `spack spec -t` output."""
+    lines = string.strip().split('\n')
+
+    result = {}
+    for line in lines:
+        match = re.match(r'\[([^]]*)\]\s*\^?([^@]*)@', line)
+        if match:
+            types, name = match.groups()
+            result.setdefault(name, []).append(types)
+            result[name] = sorted(result[name])
+    return result
+
+
+def test_spec_deptypes_nodes():
+    output = spec('--types', '--cover', 'nodes', 'dt-diamond')
+    types = _parse_types(output)
+
+    assert types['dt-diamond']        == ['    ']
+    assert types['dt-diamond-left']   == ['bl  ']
+    assert types['dt-diamond-right']  == ['bl  ']
+    assert types['dt-diamond-bottom'] == ['blr ']
+
+
+def test_spec_deptypes_edges():
+    output = spec('--types', '--cover', 'edges', 'dt-diamond')
+    types = _parse_types(output)
+
+    assert types['dt-diamond']        == ['    ']
+    assert types['dt-diamond-left']   == ['bl  ']
+    assert types['dt-diamond-right']  == ['bl  ']
+    assert types['dt-diamond-bottom'] == ['b   ', 'blr ']