Provide error message on malformed job list

In project and project-template definitions, the existing voluptuous
schema for the jobs in the job list was vs.Any(str, dict). The contents
of the dict itself need to be validated though, the job being a dict
that looks like:

    check:
      jobs:
        - project-test1:
            - required-projects:
                org/project2

Is invalid as the contents of the build-openstack-sphinx-docs job dict
should themselves be a string or a dict rather than a list. This updates
the error to be:

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

    expected str for dictionary value @ data['check']['jobs'][0]['project-test1']

  The error appears in the following project stanza:

    project:
        name: org/project1
        check:
          jobs:
            - project-test1:
                - required-projects:
                    org/project2

    in "org/project/.zuul.yaml@master", line 4, column 3

The error, 'expected str for dictionary value' could probably be
improved at some point, but this is at least an error with a message
which is way better than 'Unknown configuration error'.

Split out the attributes of the job in the JobParser voluptuous schema
that can be used in job lists from the ones that can't. For now it's
only name that can't be used.

Also fix a test fixture that had a trailing : in it.

Change-Id: I217eb5d6befbed51b220d47afa18997a87982389
diff --git a/tests/fixtures/layouts/delayed-repo-init.yaml b/tests/fixtures/layouts/delayed-repo-init.yaml
index e97d37a..c89e2fa 100644
--- a/tests/fixtures/layouts/delayed-repo-init.yaml
+++ b/tests/fixtures/layouts/delayed-repo-init.yaml
@@ -67,7 +67,7 @@
             dependencies: project-merge
     gate:
       jobs:
-        - project-merge:
+        - project-merge
         - project-test1:
             dependencies: project-merge
         - project-test2:
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 1c633ba..68ea74f 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -816,6 +816,60 @@
                       A.messages[0],
                       "A should have a syntax error reported")
 
+    def test_job_list_in_project_template_not_dict_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: project-test1
+            - project-template:
+                name: some-jobs
+                check:
+                  jobs:
+                    - project-test1:
+                        - required-projects:
+                            org/project2
+            """)
+
+        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, 1,
+                         "A should report failure")
+        self.assertIn('expected str for dictionary value',
+                      A.messages[0], "A should have a syntax error reported")
+
+    def test_job_list_in_project_not_dict_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: project-test1
+            - project:
+                name: org/project1
+                check:
+                  jobs:
+                    - project-test1:
+                        - required-projects:
+                            org/project2
+            """)
+
+        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, 1,
+                         "A should report failure")
+        self.assertIn('expected str for dictionary value',
+                      A.messages[0], "A should have a syntax error reported")
+
     def test_multi_repo(self):
         downstream_repo_conf = textwrap.dedent(
             """
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 6a9ba01..2b91966 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -11,6 +11,7 @@
 # under the License.
 
 import base64
+import collections
 from contextlib import contextmanager
 import copy
 import os
@@ -397,39 +398,42 @@
     secret = {vs.Required('name'): str,
               vs.Required('secret'): str}
 
-    job = {vs.Required('name'): str,
-           'parent': vs.Any(str, None),
-           'final': bool,
-           'failure-message': str,
-           'success-message': str,
-           'failure-url': str,
-           'success-url': str,
-           'hold-following-changes': bool,
-           'voting': bool,
-           'semaphore': str,
-           'tags': to_list(str),
-           'branches': to_list(str),
-           'files': to_list(str),
-           'secrets': to_list(vs.Any(secret, str)),
-           'irrelevant-files': to_list(str),
-           # validation happens in NodeSetParser
-           'nodeset': vs.Any(dict, str),
-           'timeout': int,
-           'attempts': int,
-           'pre-run': to_list(str),
-           'post-run': to_list(str),
-           'run': str,
-           '_source_context': model.SourceContext,
-           '_start_mark': ZuulMark,
-           'roles': to_list(role),
-           'required-projects': to_list(vs.Any(job_project, str)),
-           'vars': dict,
-           'dependencies': to_list(str),
-           'allowed-projects': to_list(str),
-           'override-branch': str,
-           'description': str,
-           'post-review': bool
-           }
+    # Attributes of a job that can also be used in Project and ProjectTemplate
+    job_attributes = {'parent': vs.Any(str, None),
+                      'final': bool,
+                      'failure-message': str,
+                      'success-message': str,
+                      'failure-url': str,
+                      'success-url': str,
+                      'hold-following-changes': bool,
+                      'voting': bool,
+                      'semaphore': str,
+                      'tags': to_list(str),
+                      'branches': to_list(str),
+                      'files': to_list(str),
+                      'secrets': to_list(vs.Any(secret, str)),
+                      'irrelevant-files': to_list(str),
+                      # validation happens in NodeSetParser
+                      'nodeset': vs.Any(dict, str),
+                      'timeout': int,
+                      'attempts': int,
+                      'pre-run': to_list(str),
+                      'post-run': to_list(str),
+                      'run': str,
+                      '_source_context': model.SourceContext,
+                      '_start_mark': ZuulMark,
+                      'roles': to_list(role),
+                      'required-projects': to_list(vs.Any(job_project, str)),
+                      'vars': dict,
+                      'dependencies': to_list(str),
+                      'allowed-projects': to_list(str),
+                      'override-branch': str,
+                      'description': str,
+                      'post-review': bool}
+
+    job_name = {vs.Required('name'): str}
+
+    job = dict(collections.ChainMap(job_name, job_attributes))
 
     schema = vs.Schema(job)
 
@@ -725,9 +729,12 @@
             '_start_mark': ZuulMark,
         }
 
+        job = {str: vs.Any(str, JobParser.job_attributes)}
+        job_list = [vs.Any(str, job)]
+        pipeline_contents = {'queue': str, 'jobs': job_list}
+
         for p in self.layout.pipelines.values():
-            project_template[p.name] = {'queue': str,
-                                        'jobs': [vs.Any(str, dict)]}
+            project_template[p.name] = pipeline_contents
         return vs.Schema(project_template)
 
     def fromYaml(self, conf, validate=True):
@@ -796,9 +803,12 @@
             '_start_mark': ZuulMark,
         }
 
+        job = {str: vs.Any(str, JobParser.job_attributes)}
+        job_list = [vs.Any(str, job)]
+        pipeline_contents = {'queue': str, 'jobs': job_list}
+
         for p in self.layout.pipelines.values():
-            project[p.name] = {'queue': str,
-                               'jobs': [vs.Any(str, dict)]}
+            project[p.name] = pipeline_contents
         return vs.Schema(project)
 
     def fromYaml(self, conf_list):