Add a promote client command

Takes one or more changes and promotes them to the head of the queue.

Also, change the command line syntax for the enqueue command to accept
change IDs in the form 'change,patchset' in order to match the syntax
of promote, as well as be potentially more compatible with future
triggers.

Change-Id: Ic7ded9587c68217c060328bf4c3518e32fe659e3
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 91d0913..c15d70c 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -2976,8 +2976,7 @@
         r = client.enqueue(pipeline='gate',
                            project='org/project',
                            trigger='gerrit',
-                           change='1',
-                           patchset='1')
+                           change='1,1')
         self.waitUntilSettled()
         self.assertEqual(self.getJobFromHistory('project-merge').result,
                          'SUCCESS')
@@ -2998,8 +2997,7 @@
             r = client.enqueue(pipeline='gate',
                                project='project-does-not-exist',
                                trigger='gerrit',
-                               change='1',
-                               patchset='1')
+                               change='1,1')
             client.shutdown()
             self.assertEqual(r, False)
 
@@ -3008,8 +3006,7 @@
             r = client.enqueue(pipeline='pipeline-does-not-exist',
                                project='org/project',
                                trigger='gerrit',
-                               change='1',
-                               patchset='1')
+                               change='1,1')
             client.shutdown()
             self.assertEqual(r, False)
 
@@ -3018,8 +3015,7 @@
             r = client.enqueue(pipeline='gate',
                                project='org/project',
                                trigger='trigger-does-not-exist',
-                               change='1',
-                               patchset='1')
+                               change='1,1')
             client.shutdown()
             self.assertEqual(r, False)
 
@@ -3028,11 +3024,164 @@
             r = client.enqueue(pipeline='gate',
                                project='org/project',
                                trigger='gerrit',
-                               change='1',
-                               patchset='1')
+                               change='1,1')
             client.shutdown()
             self.assertEqual(r, False)
 
         self.waitUntilSettled()
         self.assertEqual(len(self.history), 0)
         self.assertEqual(len(self.builds), 0)
+
+    def test_client_promote(self):
+        "Test that the RPC client can promote a change"
+        self.worker.hold_jobs_in_build = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+        C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+        A.addApproval('CRVW', 2)
+        B.addApproval('CRVW', 2)
+        C.addApproval('CRVW', 2)
+
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+
+        self.waitUntilSettled()
+
+        client = zuul.rpcclient.RPCClient('127.0.0.1',
+                                          self.gearman_server.port)
+        r = client.promote(pipeline='gate',
+                           change_ids=['2,1', '3,1'])
+
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 6)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertEqual(self.builds[2].name, 'project-test1')
+        self.assertEqual(self.builds[3].name, 'project-test2')
+        self.assertEqual(self.builds[4].name, 'project-test1')
+        self.assertEqual(self.builds[5].name, 'project-test2')
+
+        self.assertTrue(self.job_has_changes(self.builds[0], B))
+        self.assertFalse(self.job_has_changes(self.builds[0], A))
+        self.assertFalse(self.job_has_changes(self.builds[0], C))
+
+        self.assertTrue(self.job_has_changes(self.builds[2], B))
+        self.assertTrue(self.job_has_changes(self.builds[2], C))
+        self.assertFalse(self.job_has_changes(self.builds[2], A))
+
+        self.assertTrue(self.job_has_changes(self.builds[4], B))
+        self.assertTrue(self.job_has_changes(self.builds[4], C))
+        self.assertTrue(self.job_has_changes(self.builds[4], A))
+
+        self.worker.release()
+        self.waitUntilSettled()
+
+        self.assertEqual(A.data['status'], 'MERGED')
+        self.assertEqual(A.reported, 2)
+        self.assertEqual(B.data['status'], 'MERGED')
+        self.assertEqual(B.reported, 2)
+        self.assertEqual(C.data['status'], 'MERGED')
+        self.assertEqual(C.reported, 2)
+
+        client.shutdown()
+        self.assertEqual(r, True)
+
+    def test_client_promote_dependent(self):
+        "Test that the RPC client can promote a dependent change"
+        # C (depends on B) -> B -> A ; then promote C to get:
+        # A -> C (depends on B) -> B
+        self.worker.hold_jobs_in_build = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
+        C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
+
+        C.setDependsOn(B, 1)
+
+        A.addApproval('CRVW', 2)
+        B.addApproval('CRVW', 2)
+        C.addApproval('CRVW', 2)
+
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+        self.fake_gerrit.addEvent(C.addApproval('APRV', 1))
+
+        self.waitUntilSettled()
+
+        client = zuul.rpcclient.RPCClient('127.0.0.1',
+                                          self.gearman_server.port)
+        r = client.promote(pipeline='gate',
+                           change_ids=['3,1'])
+
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+        self.worker.release('.*-merge')
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.builds), 6)
+        self.assertEqual(self.builds[0].name, 'project-test1')
+        self.assertEqual(self.builds[1].name, 'project-test2')
+        self.assertEqual(self.builds[2].name, 'project-test1')
+        self.assertEqual(self.builds[3].name, 'project-test2')
+        self.assertEqual(self.builds[4].name, 'project-test1')
+        self.assertEqual(self.builds[5].name, 'project-test2')
+
+        self.assertTrue(self.job_has_changes(self.builds[0], B))
+        self.assertFalse(self.job_has_changes(self.builds[0], A))
+        self.assertFalse(self.job_has_changes(self.builds[0], C))
+
+        self.assertTrue(self.job_has_changes(self.builds[2], B))
+        self.assertTrue(self.job_has_changes(self.builds[2], C))
+        self.assertFalse(self.job_has_changes(self.builds[2], A))
+
+        self.assertTrue(self.job_has_changes(self.builds[4], B))
+        self.assertTrue(self.job_has_changes(self.builds[4], C))
+        self.assertTrue(self.job_has_changes(self.builds[4], A))
+
+        self.worker.release()
+        self.waitUntilSettled()
+
+        self.assertEqual(A.data['status'], 'MERGED')
+        self.assertEqual(A.reported, 2)
+        self.assertEqual(B.data['status'], 'MERGED')
+        self.assertEqual(B.reported, 2)
+        self.assertEqual(C.data['status'], 'MERGED')
+        self.assertEqual(C.reported, 2)
+
+        client.shutdown()
+        self.assertEqual(r, True)
+
+    def test_client_promote_negative(self):
+        "Test that the RPC client returns errors for promotion"
+        self.worker.hold_jobs_in_build = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        A.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.waitUntilSettled()
+
+        client = zuul.rpcclient.RPCClient('127.0.0.1',
+                                          self.gearman_server.port)
+
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure):
+            r = client.promote(pipeline='nonexistent',
+                               change_ids=['2,1', '3,1'])
+            client.shutdown()
+            self.assertEqual(r, False)
+
+        with testtools.ExpectedException(zuul.rpcclient.RPCFailure):
+            r = client.promote(pipeline='gate',
+                               change_ids=['4,1'])
+            client.shutdown()
+            self.assertEqual(r, False)
+
+        self.worker.hold_jobs_in_build = False
+        self.worker.release()
+        self.waitUntilSettled()