Repository: spack/spack

Issue:
dump_annotated does not print everything
### Steps to reproduce the issue

```console
$ spack config blame config
```

does not print `config:build_jobs` for me.

The problem is that `dump_annotated` populates `_annotations` and `lines`, but they are not of the same length. In my case `len(lines) == 41` and `len(_annotations) == 38`, which means that the last 3 config entries are not printed:

```allow_sgid: True', '  debug: False', '  build_jobs: 2'```

are nowhere to be seen in the output.

Maybe related: the "package_lock_timeout: null" option is printed in a weird way:

```package_lock_timeout: !!null 'None'```


Discussion:
Should this issue be part of some milestone?

@tgamblin, I had assigned this to you but I could have just pinged you in the thread as well; maybe you can shed some light on this because you've written most of the code of this command

Resolving patch:
diff --git a/lib/spack/spack/util/spack_yaml.py b/lib/spack/spack/util/spack_yaml.py
--- a/lib/spack/spack/util/spack_yaml.py
+++ b/lib/spack/spack/util/spack_yaml.py
@@ -13,6 +13,7 @@
 
 """
 import ctypes
+import re
 import sys
 from typing import List  # novm
 
@@ -185,6 +186,12 @@ def ignore_aliases(self, _data):
         """Make the dumper NEVER print YAML aliases."""
         return True
 
+    def represent_data(self, data):
+        result = super(OrderedLineDumper, self).represent_data(data)
+        if data is None:
+            result.value = syaml_str("null")
+        return result
+
     def represent_str(self, data):
         if hasattr(data, 'override') and data.override:
             data = data + ':'
@@ -262,19 +269,18 @@ def process_scalar(self):
     def represent_data(self, data):
         """Force syaml_str to be passed through with marks."""
         result = super(LineAnnotationDumper, self).represent_data(data)
-        if isinstance(result.value, string_types):
+        if data is None:
+            result.value = syaml_str("null")
+        elif isinstance(result.value, string_types):
             result.value = syaml_str(data)
         if markable(result.value):
             mark(result.value, data)
         return result
 
-    def write_stream_start(self):
-        super(LineAnnotationDumper, self).write_stream_start()
-        _annotations.append(colorize('@K{---}'))
-
     def write_line_break(self):
         super(LineAnnotationDumper, self).write_line_break()
-        if not self.saved:
+        if self.saved is None:
+            _annotations.append(colorize('@K{---}'))
             return
 
         # append annotations at the end of each line
@@ -322,7 +328,10 @@ def dump_annotated(data, stream=None, *args, **kwargs):
 
     sio = StringIO()
     yaml.dump(data, sio, *args, **kwargs)
-    lines = sio.getvalue().rstrip().split('\n')
+
+    # write_line_break() is not called by YAML for empty lines, so we
+    # skip empty lines here with \n+.
+    lines = re.split(r"\n+", sio.getvalue().rstrip())
 
     getvalue = None
     if stream is None:


Test patch:
diff --git a/lib/spack/spack/test/util/spack_yaml.py b/lib/spack/spack/test/util/spack_yaml.py
--- a/lib/spack/spack/test/util/spack_yaml.py
+++ b/lib/spack/spack/test/util/spack_yaml.py
@@ -65,3 +65,31 @@ def test_config_blame_with_override(config):
         check_blame('verify_ssl', config_file, 13)
         check_blame('checksum', config_file, 14)
         check_blame('dirty', config_file, 15)
+
+
+def test_config_blame_defaults():
+    """check blame for an element from an override scope"""
+    files = {}
+
+    def get_file_lines(filename):
+        if filename not in files:
+            with open(filename, "r") as f:
+                files[filename] = [""] + f.read().split("\n")
+        return files[filename]
+
+    config_blame = config_cmd("blame", "config")
+    for line in config_blame.split("\n"):
+        # currently checking only simple lines with dict keys
+        match = re.match(r"^([^:]+):(\d+)\s+([^:]+):\s+(.*)", line)
+
+        # check that matches are on the lines they say they are
+        if match:
+            filename, line, key, val = match.groups()
+            line = int(line)
+
+            if val.lower() in ("true", "false"):
+                val = val.lower()
+
+            lines = get_file_lines(filename)
+            assert key in lines[line]
+            assert val in lines[line]