Report YAML parse errors

The SourceContext is extended to support storing a path, and we
define our own YamlLoader to use that to include a complete
file location in the yaml error message, rather than "unicode string".

The resulting report looks like:

Zuul encountered a syntax error while parsing its configuration in the
repo org/project on branch master.  The error was:

  while parsing a block collection
  in "org/project/.zuul.yaml@master", line 2, column 1:
    - job:
    ^
expected <block end>, but found '?'
  in "org/project/.zuul.yaml@master", line 3, column 1:
    foo: error
    ^

Change-Id: I51cec01e45adaad5abeda9bf665cc0358b45486f
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 9bd405e..f10a82d 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -30,14 +30,15 @@
     def setUp(self):
         super(TestJob, self).setUp()
         self.project = model.Project('project', None)
-        self.context = model.SourceContext(self.project, 'master', True)
+        self.context = model.SourceContext(self.project, 'master',
+                                           'test', True)
 
     @property
     def job(self):
         tenant = model.Tenant('tenant')
         layout = model.Layout()
         project = model.Project('project', None)
-        context = model.SourceContext(project, 'master', True)
+        context = model.SourceContext(project, 'master', 'test', True)
         job = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': context,
             'name': 'job',
@@ -142,7 +143,7 @@
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
         project = model.Project('project', None)
-        context = model.SourceContext(project, 'master', True)
+        context = model.SourceContext(project, 'master', 'test', True)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': context,
@@ -296,7 +297,7 @@
         tenant = model.Tenant('tenant')
         layout = model.Layout()
         project = model.Project('project', None)
-        context = model.SourceContext(project, 'master', True)
+        context = model.SourceContext(project, 'master', 'test', True)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': context,
@@ -381,7 +382,7 @@
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
         project = model.Project('project', None)
-        context = model.SourceContext(project, 'master', True)
+        context = model.SourceContext(project, 'master', 'test', True)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': context,
@@ -454,7 +455,7 @@
         layout.addPipeline(pipeline)
         queue = model.ChangeQueue(pipeline)
         project = model.Project('project', None)
-        context = model.SourceContext(project, 'master', True)
+        context = model.SourceContext(project, 'master', 'test', True)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': context,
@@ -498,7 +499,8 @@
         tenant = model.Tenant('tenant')
         layout = model.Layout()
         base_project = model.Project('base_project', None)
-        base_context = model.SourceContext(base_project, 'master', True)
+        base_context = model.SourceContext(base_project, 'master',
+                                           'test', True)
 
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': base_context,
@@ -507,7 +509,8 @@
         layout.addJob(base)
 
         other_project = model.Project('other_project', None)
-        other_context = model.SourceContext(other_project, 'master', True)
+        other_context = model.SourceContext(other_project, 'master',
+                                            'test', True)
         base2 = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': other_context,
             'name': 'base',
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index ea7e85a..e566479 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -227,6 +227,26 @@
         self.assertIn('syntax error', A.messages[1],
                       "A should have a syntax error reported")
 
+    def test_untrusted_yaml_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+            foo: error
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        A.addApproval('code-review', 2)
+        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+        self.waitUntilSettled()
+
+        self.assertEqual(A.data['status'], 'NEW')
+        self.assertEqual(A.reported, 2,
+                         "A should report start and failure")
+        self.assertIn('syntax error', A.messages[1],
+                      "A should have a syntax error reported")
+
 
 class TestAnsible(AnsibleZuulTestCase):
     # A temporary class to hold new tests while others are disabled