Merge "Use timeout fixture (30 seconds) for tests"
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index afe4cf6..0ec9f88 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -634,8 +634,35 @@
       - name: plugin-triggering
         jobprefix: plugin-foobar
 
-You can pass several parameters to a template. A ``parameter`` value will be
-used for expansion of ``{parameter}`` in the template strings.
+You can pass several parameters to a template. A ``parameter`` value
+will be used for expansion of ``{parameter}`` in the template
+strings. The parameter ``name`` will be automatically provided and
+will contain the short name of the project, that is the portion of the
+project name after the last ``/`` character.
+
+Multiple templates can be combined in a project, and the jobs from all
+of those templates will be added to the project.  Individual jobs may
+also be added::
+
+  projects:
+   - name: plugin/foobar
+     template:
+      - name: plugin-triggering
+        jobprefix: plugin-foobar
+      - name: plugin-extras
+        jobprefix: plugin-foobar
+     check:
+      - foobar-extra-special-job
+
+The order of the jobs listed in the project (which only affects the
+order of jobs listed on the report) will be the jobs from each
+template in the order listed, followed by any jobs individually listed
+for the project.
+
+Note that if multiple templates are used for a project and one
+template specifies a job that is also specified in another template,
+or specified in the project itself, those jobs will be duplicated in
+the resulting project configuration.
 
 logging.conf
 ~~~~~~~~~~~~
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index dc659fb..98dfe86 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -110,6 +110,16 @@
     check:
      - '{projectname}-test1'
      - '{projectname}-test2'
+  - name: test-three-and-four
+    check:
+     - '{name}-test3'
+     - '{name}-test4'
+  - name: test-five
+    check:
+     - '{name}-{something}-test5'
+  - name: test-five-also
+    check:
+     - '{name}-{something}-test5'
 
 projects:
   - name: org/project
@@ -195,8 +205,20 @@
 
   - name: org/templated-project
     template:
-     - name: test-one-and-two
-       projectname: project
+      - name: test-one-and-two
+        projectname: project
+
+  - name: org/layered-project
+    template:
+      - name: test-one-and-two
+        projectname: project
+      - name: test-three-and-four
+      - name: test-five
+        something: foo
+      - name: test-five-also
+        something: foo
+    check:
+      - project-test6
 
   - name: org/node-project
     gate:
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index c15d70c..48f2281 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -766,6 +766,7 @@
         self.init_repo("org/one-job-project")
         self.init_repo("org/nonvoting-project")
         self.init_repo("org/templated-project")
+        self.init_repo("org/layered-project")
         self.init_repo("org/node-project")
         self.init_repo("org/conflict-project")
 
@@ -1958,6 +1959,32 @@
         self.assertEqual(self.getJobFromHistory('project-test2').result,
                          'SUCCESS')
 
+    def test_layered_templates(self):
+        "Test whether a job generated via a template can be launched"
+
+        A = self.fake_gerrit.addFakeChange(
+            'org/layered-project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertEqual(self.getJobFromHistory('project-test1').result,
+                         'SUCCESS')
+        self.assertEqual(self.getJobFromHistory('project-test2').result,
+                         'SUCCESS')
+        self.assertEqual(self.getJobFromHistory('layered-project-test3'
+                                                ).result, 'SUCCESS')
+        self.assertEqual(self.getJobFromHistory('layered-project-test4'
+                                                ).result, 'SUCCESS')
+        # test5 should run twice because two templates define it
+        test5_count = 0
+        for job in self.worker.build_history:
+            if job.name == 'layered-project-foo-test5':
+                test5_count += 1
+                self.assertEqual(job.result, 'SUCCESS')
+        self.assertEqual(test5_count, 2)
+        self.assertEqual(self.getJobFromHistory('project-test6').result,
+                         'SUCCESS')
+
     def test_dependent_changes_dequeue(self):
         "Test that dependent patches are not needlessly tested"
 
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 70c7101..bc82501 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -156,6 +156,9 @@
             # Craft the templates schemas
             schema = {v.Required('name'): v.Any(*template_names)}
             for required_param in template_parameters:
+                # special case 'name' which will be automatically provided
+                if required_param == 'name':
+                    continue
                 # add this template parameters as requirements:
                 schema.update({v.Required(required_param): str})
 
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index f8dd6e1..96bd624 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -286,16 +286,33 @@
 
         for config_project in data.get('projects', []):
             project = Project(config_project['name'])
+            shortname = config_project['name'].split('/')[-1]
 
-            for requested_template in config_project.get('template', []):
+            # This is reversed due to the prepend operation below, so
+            # the ultimate order is templates (in order) followed by
+            # statically defined jobs.
+            for requested_template in reversed(
+                config_project.get('template', [])):
                 # Fetch the template from 'project-templates'
                 tpl = project_templates.get(
                     requested_template.get('name'))
                 # Expand it with the project context
+                requested_template['name'] = shortname
                 expanded = deep_format(tpl, requested_template)
-                # Finally merge the expansion with whatever has been already
-                # defined for this project
-                config_project.update(expanded)
+                # Finally merge the expansion with whatever has been
+                # already defined for this project.  Prepend our new
+                # jobs to existing ones (which may have been
+                # statically defined or defined by other templates).
+                for pipeline in layout.pipelines.values():
+                    if pipeline.name in expanded:
+                        config_project.update(
+                            {pipeline.name: expanded[pipeline.name] +
+                             config_project.get(pipeline.name, [])})
+            # TODO: future enhancement -- add an option to the
+            # template block to indicate that duplicate jobs should be
+            # merged (especially to handle the case where they have
+            # children and you want all of the children to run after a
+            # single run of the parent).
 
             layout.projects[config_project['name']] = project
             mode = config_project.get('merge-mode', 'merge-resolve')