Add internal noop job

It does nothing.  Successfully.

This can be used in cases where a gate pipeline is required to
merge changes, but a project has no jobs to run.  This will
run a fake internal job that succeeds without wasting any worker
resources.

Change-Id: Ica0d109aaae5fd9aff6485eaba9c428491f98c60
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 5086d4d..c711394 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -694,6 +694,11 @@
 project-pyflakes are only executed if project-merge succeeds.  This
 can help avoid running unnecessary jobs.
 
+The special job named ``noop`` is internal to Zuul and will always
+return ``SUCCESS`` immediately.  This can be useful if you require
+that all changes be processed by a pipeline but a project has no jobs
+that can be run on it.
+
 .. seealso:: The OpenStack Zuul configuration for a comprehensive example: https://github.com/openstack-infra/config/blob/master/modules/openstack_project/files/zuul/layout.yaml
 
 Project Templates
diff --git a/tests/fixtures/layout-no-jobs.yaml b/tests/fixtures/layout-no-jobs.yaml
index ee8dc62..e860ad5 100644
--- a/tests/fixtures/layout-no-jobs.yaml
+++ b/tests/fixtures/layout-no-jobs.yaml
@@ -38,6 +38,6 @@
   - name: org/project
     merge-mode: cherry-pick
     check:
-      - noop
+      - gate-noop
     gate:
-      - noop
+      - gate-noop
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 1d7be4a..2d8d6bb 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -795,6 +795,7 @@
         self.init_repo("org/layered-project")
         self.init_repo("org/node-project")
         self.init_repo("org/conflict-project")
+        self.init_repo("org/noop-project")
 
         self.statsd = FakeStatsd()
         os.environ['STATSD_HOST'] = 'localhost'
@@ -2654,6 +2655,19 @@
         self.assertEqual(len(self.history), 10)
         self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
 
+    def test_noop_job(self):
+        "Test that the internal noop job works"
+        A = self.fake_gerrit.addFakeChange('org/noop-project', 'master', 'A')
+        A.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.gearman_server.getQueue()), 0)
+        self.assertTrue(self.sched._areAllBuildsComplete())
+        self.assertEqual(len(self.history), 0)
+        self.assertEqual(A.data['status'], 'MERGED')
+        self.assertEqual(A.reported, 2)
+
     def test_zuul_refs(self):
         "Test that zuul refs exist and have the right changes"
         self.worker.hold_jobs_in_build = True
@@ -2858,6 +2872,11 @@
 
     def test_stuck_job_cleanup(self):
         "Test that pending jobs are cleaned up if removed from layout"
+        # This job won't be registered at startup because it is not in
+        # the standard layout, but we need it to already be registerd
+        # for when we reconfigure, as that is when Zuul will attempt
+        # to run the new job.
+        self.worker.registerFunction('build:gate-noop')
         self.gearman_server.hold_jobs_in_queue = True
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         A.addApproval('CRVW', 2)
@@ -2870,13 +2889,13 @@
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
-        self.gearman_server.release('noop')
+        self.gearman_server.release('gate-noop')
         self.waitUntilSettled()
         self.assertEqual(len(self.gearman_server.getQueue()), 0)
         self.assertTrue(self.sched._areAllBuildsComplete())
 
         self.assertEqual(len(self.history), 1)
-        self.assertEqual(self.history[0].name, 'noop')
+        self.assertEqual(self.history[0].name, 'gate-noop')
         self.assertEqual(self.history[0].result, 'SUCCESS')
 
     def test_file_jobs(self):
diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py
index 3638add..7dd3c7c 100644
--- a/zuul/launcher/gearman.py
+++ b/zuul/launcher/gearman.py
@@ -290,6 +290,11 @@
         build = Build(job, uuid)
         build.parameters = params
 
+        if job.name == 'noop':
+            build.result = 'SUCCESS'
+            self.sched.onBuildCompleted(build)
+            return build
+
         gearman_job = gear.Job(name, json.dumps(params),
                                unique=uuid)
         build.__gearman_job = gearman_job