Merge "Remove unused setup_tables" into feature/zuulv3
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index b9c9b32..1f401d0 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -935,6 +935,27 @@
         self.assertIn('not a dictionary', A.messages[0],
                       "A should have a syntax error reported")
 
+    def test_yaml_duplicate_key_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: foo
+                name: bar
+            """)
+
+        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('appears more than once', A.messages[0],
+                      "A should have a syntax error reported")
+
     def test_yaml_key_error(self):
         in_repo_conf = textwrap.dedent(
             """
diff --git a/zuul/configloader.py b/zuul/configloader.py
index fb1695c..227e352 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -152,6 +152,40 @@
         super(ProjectNotPermittedError, self).__init__(message)
 
 
+class YAMLDuplicateKeyError(ConfigurationSyntaxError):
+    def __init__(self, key, node, context, start_mark):
+        intro = textwrap.fill(textwrap.dedent("""\
+        Zuul encountered a syntax error while parsing its configuration in the
+        repo {repo} on branch {branch}.  The error was:""".format(
+            repo=context.project.name,
+            branch=context.branch,
+        )))
+
+        e = textwrap.fill(textwrap.dedent("""\
+        The key "{key}" appears more than once; duplicate keys are not
+        permitted.
+        """.format(
+            key=key,
+        )))
+
+        m = textwrap.dedent("""\
+        {intro}
+
+        {error}
+
+        The error appears in the following stanza:
+
+        {content}
+
+        {start_mark}""")
+
+        m = m.format(intro=intro,
+                     error=indent(str(e)),
+                     content=indent(start_mark.snippet.rstrip()),
+                     start_mark=str(start_mark))
+        super(YAMLDuplicateKeyError, self).__init__(m)
+
+
 def indent(s):
     return '\n'.join(['  ' + x for x in s.split('\n')])
 
@@ -249,6 +283,14 @@
         self.zuul_stream = stream
 
     def construct_mapping(self, node, deep=False):
+        keys = set()
+        for k, v in node.value:
+            if k.value in keys:
+                mark = ZuulMark(node.start_mark, node.end_mark,
+                                self.zuul_stream)
+                raise YAMLDuplicateKeyError(k.value, node, self.zuul_context,
+                                            mark)
+            keys.add(k.value)
         r = super(ZuulSafeLoader, self).construct_mapping(node, deep)
         keys = frozenset(r.keys())
         if len(keys) == 1 and keys.intersection(self.zuul_node_types):
diff --git a/zuul/model.py b/zuul/model.py
index 56d08a1..e53a357 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -2443,6 +2443,8 @@
 class Layout(object):
     """Holds all of the Pipelines."""
 
+    log = logging.getLogger("zuul.layout")
+
     def __init__(self, tenant):
         self.uuid = uuid4().hex
         self.tenant = tenant
@@ -2553,7 +2555,11 @@
         matched = False
         for variant in self.getJobs(jobname):
             if not variant.changeMatches(change):
+                self.log.debug("Variant %s did not match %s", repr(variant),
+                               change)
                 continue
+            else:
+                self.log.debug("Variant %s matched %s", repr(variant), change)
             if not variant.isBase():
                 parent = variant.parent
                 if not jobs and parent is None:
@@ -2576,9 +2582,12 @@
         for jobname in job_list.jobs:
             # This is the final job we are constructing
             frozen_job = None
+            self.log.debug("Collecting jobs %s for %s", jobname, change)
             try:
                 variants = self.collectJobs(jobname, change)
             except NoMatchingParentError:
+                self.log.debug("No matching parents for job %s and change %s",
+                               jobname, change)
                 variants = None
             if not variants:
                 # A change must match at least one defined job variant
@@ -2600,6 +2609,11 @@
                 if variant.changeMatches(change):
                     frozen_job.applyVariant(variant)
                     matched = True
+                    self.log.debug("Pipeline variant %s matched %s",
+                                   repr(variant), change)
+            else:
+                self.log.debug("Pipeline variant %s did not match %s",
+                               repr(variant), change)
             if not matched:
                 # A change must match at least one project pipeline
                 # job variant.