Pass assigned nodes to the launcher

This fleshes out the nodepool stub a little more.  It passes some
node information to the launcher after node requests have been
fulfilled.  It also corrects some logic errors in the node request
framework.  It moves data structures related to node requests into
the model.  Finally, it adds nodes to the configuration of some
tests to exercise the system, and adds a test to verify the correct
node is supplied on a job that has a branch variant.

Change-Id: I395ce23ae865df3a55436ee92d04e0eae07c963a
diff --git a/tests/base.py b/tests/base.py
index 552bdd6..49ba04e 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -541,14 +541,21 @@
 class FakeBuild(object):
     log = logging.getLogger("zuul.test")
 
-    def __init__(self, launch_server, job, node):
+    def __init__(self, launch_server, job):
         self.daemon = True
         self.launch_server = launch_server
         self.job = job
         self.jobdir = None
         self.uuid = job.unique
-        self.node = node
         self.parameters = json.loads(job.arguments)
+        # TODOv3(jeblair): self.node is really "the image of the node
+        # assigned".  We should rename it (self.node_image?) if we
+        # keep using it like this, or we may end up exposing more of
+        # the complexity around multi-node jobs here
+        # (self.nodes[0].image?)
+        self.node = None
+        if len(self.parameters.get('nodes')) == 1:
+            self.node = self.parameters['nodes'][0]['image']
         self.unique = self.parameters['ZUUL_UUID']
         self.name = self.parameters['job']
         self.wait_condition = threading.Condition()
@@ -707,8 +714,7 @@
                        (regex, len(self.running_builds)))
 
     def launchJob(self, job):
-        node = None
-        build = FakeBuild(self, job, node)
+        build = FakeBuild(self, job)
         job.build = build
         self.running_builds.append(build)
         self.job_builds[job.unique] = build
diff --git a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
index 7ca8504..01de2aa 100644
--- a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
@@ -42,6 +42,16 @@
 
 - job:
     name: project-test1
+    nodes:
+      - name: controller
+        image: image1
+
+- job:
+    name: project-test1
+    branches: stable
+    nodes:
+      - name: controller
+        image: image2
 
 - job:
     name: project-test2
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index afd4027..4cce777 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -61,6 +61,8 @@
                          'SUCCESS')
         self.assertEqual(A.data['status'], 'MERGED')
         self.assertEqual(A.reported, 2)
+        self.assertEqual(self.getJobFromHistory('project-test1').node,
+                         'image1')
 
         # TODOv3(jeblair): we may want to report stats by tenant (also?).
         self.assertReportedStat('gerrit.event.comment-added', value='1|c')
@@ -88,6 +90,25 @@
         self.assertReportedStat('zuul.pipeline.check.current_changes',
                                 value='0|g')
 
+    def test_job_branch(self):
+        "Test the correct variant of a job runs on a branch"
+        self.create_branch('org/project', 'stable')
+        A = self.fake_gerrit.addFakeChange('org/project', 'stable', 'A')
+        A.addApproval('code-review', 2)
+        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+        self.waitUntilSettled()
+        self.assertEqual(self.getJobFromHistory('project-test1').result,
+                         'SUCCESS')
+        self.assertEqual(self.getJobFromHistory('project-test2').result,
+                         'SUCCESS')
+        self.assertEqual(A.data['status'], 'MERGED')
+        self.assertEqual(A.reported, 2,
+                         "A should report start and success")
+        self.assertIn('gate', A.messages[1],
+                      "A should transit gate")
+        self.assertEqual(self.getJobFromHistory('project-test1').node,
+                         'image2')
+
     @skip("Disabled for early v3 development")
     def test_duplicate_pipelines(self):
         "Test that a change matching multiple pipelines works"