Wait for a commit to appear in the repo.

This fixes a potential race condition where a change may be reported
as merged in gerrit, but due to replication, the commit may not
yet show up in the repo.  If a job is started during this time frame,
then the git repo will not be in the expected state for testing.

This now waits for the commit associated with the latest patchset
for the change to become the commit at ref/heads/branch.  This
check obviously only works to verify that the merged commit is the
_most recently_ merged commit (ie, it does not perform the more
general check that the commit was merged at some time in the past).
That's okay here because zuul's merge commands are issued
synchronously from a single thread, so if zuul instructs gerrit
to merge a change, it should become that head, and no other changes
should be able to merge during that time.

Change-Id: I2213916c4850e7be80795cbed2dac29eacdb82bf
Reviewed-on: https://review.openstack.org/10673
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index a376cd3..a61e13c 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -18,12 +18,16 @@
 import ConfigParser
 import os
 import Queue
+import hashlib
 import logging
+import random
 import json
 import threading
 import time
 import pprint
 import re
+import urllib2
+import urlparse
 
 import zuul
 import zuul.scheduler
@@ -41,12 +45,17 @@
 logging.basicConfig(level=logging.DEBUG)
 
 
+def random_sha1():
+    return hashlib.sha1(str(random.random())).hexdigest()
+
+
 class FakeChange(object):
     categories = {'APRV': ('Approved', -1, 1),
                   'CRVW': ('Code-Review', -2, 2),
                   'VRFY': ('Verified', -2, 2)}
 
-    def __init__(self, number, project, branch, subject, status='NEW'):
+    def __init__(self, gerrit, number, project, branch, subject, status='NEW'):
+        self.gerrit = gerrit
         self.reported = 0
         self.queried = 0
         self.patchsets = []
@@ -62,7 +71,7 @@
             'comments': [],
             'commitMessage': subject,
             'createdOn': time.time(),
-            'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
+            'id': 'I' + random_sha1(),
             'lastUpdated': time.time(),
             'number': str(number),
             'open': True,
@@ -90,8 +99,7 @@
              'number': str(self.latest_patchset),
              'ref': 'refs/changes/1/%s/%s' % (self.number,
                                               self.latest_patchset),
-             'revision':
-                 'aa69c46accf97d0598111724a38250ae76a22c87',
+             'revision': random_sha1(),
              'uploader': {'email': 'user@example.com',
                           'name': 'User name',
                           'username': 'user'}}
@@ -190,6 +198,8 @@
     def setMerged(self):
         self.data['status'] = 'MERGED'
         self.open = False
+        branch_heads = self.gerrit.heads[self.project]
+        branch_heads[self.branch] = self.patchsets[-1]['revision']
 
     def setReported(self):
         self.reported += 1
@@ -201,10 +211,16 @@
         self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
         self.change_number = 0
         self.changes = {}
+        self.heads = {}
 
     def addFakeChange(self, project, branch, subject):
+        branch_heads = self.heads.get(project, {})
+        if branch not in branch_heads:
+            branch_heads[branch] = random_sha1()
+        self.heads[project] = branch_heads
+
         self.change_number += 1
-        c = FakeChange(self.change_number, project, branch, subject)
+        c = FakeChange(self, self.change_number, project, branch, subject)
         self.changes[self.change_number] = c
         return c
 
@@ -441,6 +457,25 @@
         pass
 
 
+class FakeURLOpener(object):
+    def __init__(self, fake_gerrit, url):
+        self.fake_gerrit = fake_gerrit
+        self.url = url
+
+    def read(self):
+        res = urlparse.urlparse(self.url)
+        path = res.path
+        project = '/'.join(path.split('/')[2:-2])
+        heads = self.fake_gerrit.heads[project]
+        ret = ''
+        for change in self.fake_gerrit.changes.values():
+            for ps in change.patchsets:
+                ret += ps['revision'] + '\t' + ps['ref'] + '\n'
+        for head, ref in heads.items():
+            ret += ref + '\trefs/heads/' + head + '\n'
+        return ret
+
+
 class testScheduler(unittest.TestCase):
     log = logging.getLogger("zuul.test")
 
@@ -456,14 +491,21 @@
             self.fake_jenkins_callback = FakeJenkinsCallback(*args, **kw)
             return self.fake_jenkins_callback
 
+        def URLOpenerFactory(*args, **kw):
+            args = [self.fake_gerrit] + list(args)
+            return FakeURLOpener(*args, **kw)
+
         zuul.launcher.jenkins.ExtendedJenkins = jenkinsFactory
         zuul.launcher.jenkins.JenkinsCallback = jenkinsCallbackFactory
+        urllib2.urlopen = URLOpenerFactory
         self.jenkins = zuul.launcher.jenkins.Jenkins(self.config, self.sched)
         self.fake_jenkins.callback = self.fake_jenkins_callback
 
         zuul.lib.gerrit.Gerrit = FakeGerrit
 
         self.gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
+        self.gerrit.replication_timeout = 1.5
+        self.gerrit.replication_retry_interval = 0.5
         self.fake_gerrit = self.gerrit.gerrit
 
         self.sched.setLauncher(self.jenkins)