Add job keyval support to Test Suites for job tracking.

Update dynamic_suite to accept a results_dir variable, when
present write out job keyvals to the results dir that the job is running in.

These keyvals appear during the final parse of the job and are not available
anytime sooner.

These results can be read from TKO via get_detailed_test_views.

TEST=Unitests and end to end tests on the devserver
BUG=chromium-os:26857

Change-Id: I638ad27d0c38594e0c2d75f9b7236a72fa808ffb
Reviewed-on: https://gerrit.chromium.org/gerrit/16986
Commit-Ready: Scott Zawalski <scottz@chromium.org>
Reviewed-by: Scott Zawalski <scottz@chromium.org>
Tested-by: Scott Zawalski <scottz@chromium.org>
diff --git a/server/cros/dynamic_suite.py b/server/cros/dynamic_suite.py
index 803d350..6211cbe 100644
--- a/server/cros/dynamic_suite.py
+++ b/server/cros/dynamic_suite.py
@@ -264,8 +264,8 @@
 
 
     @staticmethod
-    def create_from_name(name, build, cf_getter=None,
-                         afe=None, tko=None, pool=None):
+    def create_from_name(name, build, cf_getter=None, afe=None, tko=None,
+                         pool=None, results_dir=None):
         """
         Create a Suite using a predicate based on the SUITE control file var.
 
@@ -282,16 +282,19 @@
         @param tko: an instance of TKO as defined in server/frontend.py.
         @param pool: Specify the pool of machines to use for scheduling
                      purposes.
+        @param results_dir: The directory where the job can write results to.
+                            This must be set if you want job_id of sub-jobs
+                            list in the job keyvals.
         @return a Suite instance.
         """
         if cf_getter is None:
             cf_getter = Suite.create_ds_getter(build)
         return Suite(Suite.name_in_tag_predicate(name),
-                     name, build, cf_getter, afe, tko, pool)
+                     name, build, cf_getter, afe, tko, pool, results_dir)
 
 
     def __init__(self, predicate, tag, build, cf_getter, afe=None, tko=None,
-                 pool=None):
+                 pool=None, results_dir=None):
         """
         Constructor
 
@@ -305,11 +308,15 @@
         @param tko: an instance of TKO as defined in server/frontend.py.
         @param pool: Specify the pool of machines to use for scheduling
                 purposes.
+        @param results_dir: The directory where the job can write results to.
+                            This must be set if you want job_id of sub-jobs
+                            list in the job keyvals.
         """
         self._predicate = predicate
         self._tag = tag
         self._build = build
         self._cf_getter = cf_getter
+        self._results_dir = results_dir
         self._afe = afe or frontend_wrappers.RetryingAFE(timeout_min=30,
                                                          delay_sec=10,
                                                          debug=False)
@@ -360,7 +367,6 @@
         else:
             # No pool specified use any machines with the following label.
             meta_hosts = VERSION_PREFIX + self._build
-
         return self._afe.create_job(
             control_file=test.text,
             name='/'.join([self._build, self._tag, test.name]),
@@ -427,6 +433,18 @@
             for test in self.unstable_tests():
                 logging.debug('Scheduling %s', test.name)
                 self._jobs.append(self._create_job(test))
+        if self._results_dir:
+            self._record_scheduled_jobs()
+
+
+    def _record_scheduled_jobs(self):
+        """
+        Record scheduled job ids as keyvals, so they can be referenced later.
+
+        """
+        for job in self._jobs:
+            job_id_owner = '%s-%s' % (job.id, job.owner)
+            utils.write_keyval(self.results_dir, {test.name: job_id_owner})
 
 
     def _status_is_relevant(self, status):
diff --git a/server/cros/dynamic_suite_unittest.py b/server/cros/dynamic_suite_unittest.py
index 04ca80a..fa3d348 100755
--- a/server/cros/dynamic_suite_unittest.py
+++ b/server/cros/dynamic_suite_unittest.py
@@ -373,6 +373,21 @@
         suite.schedule()
 
 
+    def testScheduleTestsAndRecord(self):
+        """Should schedule stable and experimental tests with the AFE."""
+        self.mock_control_file_parsing()
+        self.mox.ReplayAll()
+        suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
+                                                     afe=self.afe, tko=self.tko,
+                                                     results_dir=self.tmpdir)
+        self.mox.ResetAll()
+        self.expect_job_scheduling(add_experimental=True)
+        self.mox.StubOutWithMock(suite, '_record_scheduled_jobs')
+        suite._record_scheduled_jobs()
+        self.mox.ReplayAll()
+        suite.schedule()
+
+
     def testScheduleStableTests(self):
         """Should schedule only stable tests with the AFE."""
         self.mock_control_file_parsing()
@@ -400,7 +415,7 @@
         """
         self.expect_control_file_parsing()
         self.mox.ReplayAll()
-        suite = dynamic_suite.Suite.create_from_name(self._TAG, self.tmpdir,
+        suite = dynamic_suite.Suite.create_from_name(self._TAG, self._BUILD,
                                                      self.getter, self.afe,
                                                      self.tko)
         self.mox.ResetAll()