Add layout file validation.

Based on voluptuous library.

Basic validation should catch typos, missing or extra attributes.
Can be expanded to do more serious validation (ie, specifying
a comment in a trigger should require the event be comment-added).

Adds a command line option to validate a named layout file and
exit.

(Also add dist/ to .gitignore.)

Change-Id: Ia864ebde1765141d4d1a52bc77033689b6210e81
Reviewed-on: https://review.openstack.org/19443
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Reviewed-by: Jeremy Stanley <fungi@yuggoth.org>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/tests/fixtures/layouts/bad_pipelines b/tests/fixtures/layouts/bad_pipelines
new file mode 100644
index 0000000..f627208
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines
@@ -0,0 +1 @@
+pipelines:
diff --git a/tests/fixtures/layouts/bad_pipelines1.yaml b/tests/fixtures/layouts/bad_pipelines1.yaml
new file mode 100644
index 0000000..4207a2c
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines1.yaml
@@ -0,0 +1,4 @@
+pipelines:
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines10.yaml b/tests/fixtures/layouts/bad_pipelines10.yaml
new file mode 100644
index 0000000..5248c17
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines10.yaml
@@ -0,0 +1,7 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+
+projects:
+  - name: foo
+    merge-mode: foo
\ No newline at end of file
diff --git a/tests/fixtures/layouts/bad_pipelines2.yaml b/tests/fixtures/layouts/bad_pipelines2.yaml
new file mode 100644
index 0000000..e75a561
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines2.yaml
@@ -0,0 +1,6 @@
+pipelines:
+  - noname: check
+    manager: IndependentPipelineManager
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines3.yaml b/tests/fixtures/layouts/bad_pipelines3.yaml
new file mode 100644
index 0000000..0c11a85
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines3.yaml
@@ -0,0 +1,6 @@
+pipelines:
+  - name: check
+    manager: NonexistentPipelineManager
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines4.yaml b/tests/fixtures/layouts/bad_pipelines4.yaml
new file mode 100644
index 0000000..a99b9e2
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines4.yaml
@@ -0,0 +1,8 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    trigger:
+      - event: non-event
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines5.yaml b/tests/fixtures/layouts/bad_pipelines5.yaml
new file mode 100644
index 0000000..7db7bd1
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines5.yaml
@@ -0,0 +1,9 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    trigger:
+      - approval:
+          - approved: 1
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines6.yaml b/tests/fixtures/layouts/bad_pipelines6.yaml
new file mode 100644
index 0000000..8d313bc
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines6.yaml
@@ -0,0 +1,9 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    trigger:
+      - event: comment-added
+        approved: 1
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines7.yaml b/tests/fixtures/layouts/bad_pipelines7.yaml
new file mode 100644
index 0000000..7517b9a
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines7.yaml
@@ -0,0 +1,5 @@
+pipelines:
+  - manager: IndependentPipelineManager
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines8.yaml b/tests/fixtures/layouts/bad_pipelines8.yaml
new file mode 100644
index 0000000..eeab038
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines8.yaml
@@ -0,0 +1,5 @@
+pipelines:
+  - name: check
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_pipelines9.yaml b/tests/fixtures/layouts/bad_pipelines9.yaml
new file mode 100644
index 0000000..ebb2e1f
--- /dev/null
+++ b/tests/fixtures/layouts/bad_pipelines9.yaml
@@ -0,0 +1,8 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+  - name: check
+    manager: IndependentPipelineManager
+
+projects:
+  - name: foo
diff --git a/tests/fixtures/layouts/bad_projects1.yaml b/tests/fixtures/layouts/bad_projects1.yaml
new file mode 100644
index 0000000..c210c43
--- /dev/null
+++ b/tests/fixtures/layouts/bad_projects1.yaml
@@ -0,0 +1,9 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+
+projects:
+  - name: foo
+    gate:
+      - test
+
diff --git a/tests/fixtures/layouts/bad_projects2.yaml b/tests/fixtures/layouts/bad_projects2.yaml
new file mode 100644
index 0000000..b91ed9d
--- /dev/null
+++ b/tests/fixtures/layouts/bad_projects2.yaml
@@ -0,0 +1,9 @@
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+
+projects:
+  - name: foo
+    check:
+      - test
+        - foo
diff --git a/tests/fixtures/layouts/good_layout.yaml b/tests/fixtures/layouts/good_layout.yaml
new file mode 100644
index 0000000..ca024ec
--- /dev/null
+++ b/tests/fixtures/layouts/good_layout.yaml
@@ -0,0 +1,58 @@
+includes:
+  - python-file: openstack_functions.py
+
+pipelines:
+  - name: check
+    manager: IndependentPipelineManager
+    trigger:
+      - event: patchset-created
+    success:
+      verified: 1
+    failure:
+      verified: -1
+
+  - name: post
+    manager: IndependentPipelineManager
+    trigger:
+      - event: ref-updated
+        ref: ^(?!refs/).*$
+
+  - name: gate
+    manager: DependentPipelineManager
+    trigger:
+      - event: comment-added
+        approval:
+          - approved: 1
+    success:
+      verified: 2
+      code-review: 1
+      submit: true
+    failure:
+      verified: -2
+      workinprogress: true
+    start:
+      verified: 0
+
+jobs:
+  - name: ^.*-merge$
+    failure-message: Unable to merge change
+    hold-following-changes: true
+  - name: test-merge
+    parameter-function: devstack_params
+  - name: test-test
+  - name: test-merge2
+    success-pattern: http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}/success
+    failure-pattern: http://logs.example.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.number}/fail
+
+projects:
+  - name: test-org/test
+    merge-mode: cherry-pick
+    check:
+      - test-merge2:
+          - test-thing1:
+              - test-thing2
+              - test-thing3
+    gate:
+      - test-thing
+    post:
+      - test-post
diff --git a/tests/test_layoutvalidator.py b/tests/test_layoutvalidator.py
new file mode 100644
index 0000000..343dc47
--- /dev/null
+++ b/tests/test_layoutvalidator.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+# Copyright 2013 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import unittest
+import os
+import re
+import yaml
+import voluptuous
+
+import zuul.layoutvalidator
+
+FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
+                           'fixtures')
+LAYOUT_RE = re.compile(r'^(good|bad)_.*\.yaml$')
+
+
+class testScheduler(unittest.TestCase):
+    def test_layouts(self):
+        """Test layout file validation"""
+        print
+        errors = []
+        for fn in os.listdir(os.path.join(FIXTURE_DIR, 'layouts')):
+            m = LAYOUT_RE.match(fn)
+            if not m:
+                continue
+            print fn
+            layout = os.path.join(FIXTURE_DIR, 'layouts', fn)
+            data = yaml.load(open(layout))
+            validator = zuul.layoutvalidator.LayoutValidator()
+            if m.group(1) == 'good':
+                try:
+                    validator.validate(data)
+                except voluptuous.Invalid, e:
+                    raise Exception(
+                        'Unexpected YAML syntax error in %s:\n  %s' %
+                        (fn, str(e)))
+            else:
+                try:
+                    validator.validate(data)
+                    raise Exception("Expected a YAML syntax error in %s." %
+                                    fn)
+                except voluptuous.Invalid, e:
+                    error = str(e)
+                    print '  ', error
+                    if error in errors:
+                        raise Exception("Error has already beed tested: %s" %
+                                        error)
+                    else:
+                        errors.append(error)
+                    pass