Git driver

This patch improves the existing git driver by adding
a refs watcher thread. This refs watcher looks at
refs added, deleted, updated and trigger a ref-updated
event.

When a refs is updated and that the related commits
from oldrev to newrev include a change on .zuul.yaml/zuul.yaml
or zuul.d/*.yaml then tenants including that ref is reconfigured.

Furthermore the patch includes a triggering model. Events are
sent to the scheduler so jobs can be attached to a pipeline for
running jobs.

Change-Id: I529660cb20d011f36814abe64f837945dd3f1f33
diff --git a/tests/unit/test_git_driver.py b/tests/unit/test_git_driver.py
index 1cfadf4..b9e6c6e 100644
--- a/tests/unit/test_git_driver.py
+++ b/tests/unit/test_git_driver.py
@@ -12,7 +12,12 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-from tests.base import ZuulTestCase
+
+import os
+import time
+import yaml
+
+from tests.base import ZuulTestCase, simple_layout
 
 
 class TestGitDriver(ZuulTestCase):
@@ -23,7 +28,7 @@
         super(TestGitDriver, self).setup_config()
         self.config.set('connection git', 'baseurl', self.upstream_root)
 
-    def test_git_driver(self):
+    def test_basic(self):
         tenant = self.sched.abide.tenants.get('tenant-one')
         # Check that we have the git source for common-config and the
         # gerrit source for the project.
@@ -40,3 +45,127 @@
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 1)
         self.assertEqual(A.reported, 1)
+
+    def test_config_refreshed(self):
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(len(self.history), 1)
+        self.assertEqual(A.reported, 1)
+        self.assertEqual(self.history[0].name, 'project-test1')
+
+        # Update zuul.yaml to force a tenant reconfiguration
+        path = os.path.join(self.upstream_root, 'common-config', 'zuul.yaml')
+        config = yaml.load(open(path, 'r').read())
+        change = {
+            'name': 'org/project',
+            'check': {
+                'jobs': [
+                    'project-test2'
+                ]
+            }
+        }
+        config[4]['project'] = change
+        files = {'zuul.yaml': yaml.dump(config)}
+        self.addCommitToRepo(
+            'common-config', 'Change zuul.yaml configuration', files)
+
+        # Let some time for the tenant reconfiguration to happen
+        time.sleep(2)
+        self.waitUntilSettled()
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(len(self.history), 2)
+        self.assertEqual(A.reported, 1)
+        # We make sure the new job has run
+        self.assertEqual(self.history[1].name, 'project-test2')
+
+        # Let's stop the git Watcher to let us merge some changes commits
+        # We want to verify that config changes are detected for commits
+        # on the range oldrev..newrev
+        self.sched.connections.getSource('git').connection.w_pause = True
+        # Add a config change
+        change = {
+            'name': 'org/project',
+            'check': {
+                'jobs': [
+                    'project-test1'
+                ]
+            }
+        }
+        config[4]['project'] = change
+        files = {'zuul.yaml': yaml.dump(config)}
+        self.addCommitToRepo(
+            'common-config', 'Change zuul.yaml configuration', files)
+        # Add two other changes
+        self.addCommitToRepo(
+            'common-config', 'Adding f1',
+            {'f1': "Content"})
+        self.addCommitToRepo(
+            'common-config', 'Adding f2',
+            {'f2': "Content"})
+        # Restart the git watcher
+        self.sched.connections.getSource('git').connection.w_pause = False
+
+        # Let some time for the tenant reconfiguration to happen
+        time.sleep(2)
+        self.waitUntilSettled()
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(len(self.history), 3)
+        self.assertEqual(A.reported, 1)
+        # We make sure the new job has run
+        self.assertEqual(self.history[2].name, 'project-test1')
+
+    def ensure_watcher_has_context(self):
+        # Make sure watcher have read initial refs shas
+        cnx = self.sched.connections.getSource('git').connection
+        delay = 0.1
+        max_delay = 1
+        while not cnx.projects_refs:
+            time.sleep(delay)
+            max_delay -= delay
+            if max_delay <= 0:
+                raise Exception("Timeout waiting for initial read")
+
+    @simple_layout('layouts/basic-git.yaml', driver='git')
+    def test_ref_updated_event(self):
+        self.ensure_watcher_has_context()
+        # Add a commit to trigger a ref-updated event
+        self.addCommitToRepo(
+            'org/project', 'A change for ref-updated', {'f1': 'Content'})
+        # Let some time for the git watcher to detect the ref-update event
+        time.sleep(0.2)
+        self.waitUntilSettled()
+        self.assertEqual(len(self.history), 1)
+        self.assertEqual('SUCCESS',
+                         self.getJobFromHistory('post-job').result)
+
+    @simple_layout('layouts/basic-git.yaml', driver='git')
+    def test_ref_created(self):
+        self.ensure_watcher_has_context()
+        # Tag HEAD to trigger a ref-updated event
+        self.addTagToRepo(
+            'org/project', 'atag', 'HEAD')
+        # Let some time for the git watcher to detect the ref-update event
+        time.sleep(0.2)
+        self.waitUntilSettled()
+        self.assertEqual(len(self.history), 1)
+        self.assertEqual('SUCCESS',
+                         self.getJobFromHistory('tag-job').result)
+
+    @simple_layout('layouts/basic-git.yaml', driver='git')
+    def test_ref_deleted(self):
+        self.ensure_watcher_has_context()
+        # Delete default tag init to trigger a ref-updated event
+        self.delTagFromRepo(
+            'org/project', 'init')
+        # Let some time for the git watcher to detect the ref-update event
+        time.sleep(0.2)
+        self.waitUntilSettled()
+        # Make sure no job as run as ignore-delete is True by default
+        self.assertEqual(len(self.history), 0)