Add friendly error messages and tests for nodeset dupes

Report nice error messages for duplicate node or group names within
a nodeset.

Also add some missing implicit list conversions for single-item
lists.

Change-Id: I86d63e922c459fb7bee60f9f7ff377ce9ed9e5ed
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 707515a..d15189d 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -368,6 +368,59 @@
         self.assertIn('the only project definition permitted', A.messages[0],
                       "A should have a syntax error reported")
 
+    def test_duplicate_node_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - nodeset:
+                name: duplicate
+                nodes:
+                  - name: compute
+                    image: foo
+                  - name: compute
+                    image: foo
+            """)
+
+        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 multiple times', A.messages[0],
+                      "A should have a syntax error reported")
+
+    def test_duplicate_group_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - nodeset:
+                name: duplicate
+                nodes:
+                  - name: compute
+                    image: foo
+                groups:
+                  - name: group
+                    nodes: compute
+                  - name: group
+                    nodes: compute
+            """)
+
+        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 multiple times', A.messages[0],
+                      "A should have a syntax error reported")
+
 
 class TestAnsible(AnsibleZuulTestCase):
     # A temporary class to hold new tests while others are disabled