[autotest] Add a harness for running dynamic suites from your workstation

To enable development on the dynamic suite infrastructure.

BUG=None
TEST=./server/autoserv test_suites/dev_harness

Change-Id: Ib2a63d19f919f5e256f0a36dbca33e1b52f92875
Reviewed-on: https://gerrit.chromium.org/gerrit/13240
Commit-Ready: Chris Masone <cmasone@chromium.org>
Reviewed-by: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
Reviewed-by: Scott Zawalski <scottz@chromium.org>
diff --git a/global_config.ini b/global_config.ini
index 32dcbb1..37fa226 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -130,3 +130,9 @@
 
 [CROS]
 source_tree: /usr/local/google/chromeos
+dev_server: http://172.22.50.205:8080
+# The below should be %(dev_server)s/update/%%(name)s so that we'd fill in
+# the above value for dev_server when this config is parsed, but leave the
+# name field to be populated later.  Sadly, that doesn't parse.
+image_url_pattern: %(dev_server)s/update/%%s
+package_url_pattern: %(dev_server)s/static/archive/%%s/autotest
diff --git a/server/cros/dynamic_suite.py b/server/cros/dynamic_suite.py
index 066661d..0104416 100644
--- a/server/cros/dynamic_suite.py
+++ b/server/cros/dynamic_suite.py
@@ -4,12 +4,23 @@
 
 import common
 import compiler, logging, os, random, re, time
-from autotest_lib.client.common_lib import control_data, error, utils
+from autotest_lib.client.common_lib import control_data, global_config, error
+from autotest_lib.client.common_lib import utils
 from autotest_lib.server.cros import control_file_getter
 from autotest_lib.server import frontend
 
 
 VERSION_PREFIX = 'cros-version-'
+CONFIG = global_config.global_config
+
+
+def _image_url_pattern():
+    return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
+
+
+def _package_url_pattern():
+    return CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
+
 
 class Reimager(object):
     """
@@ -35,7 +46,11 @@
             [os.path.join(autotest_dir, 'server/site_tests')])
 
 
-    def attempt(self, url, name, num, board, record):
+    def skip(self, g):
+        return 'SKIP_IMAGE' in g and g['SKIP_IMAGE']
+
+
+    def attempt(self, name, num, board, record):
         """
         Synchronously attempt to reimage some machines.
 
@@ -43,7 +58,6 @@
         image at |url| called |name|.  Wait for completion, polling every
         10s, and log results with |record| upon completion.
 
-        @param url: the URL of the image to install.
         @param name: the name of the image to install (must be unique).
         @param num: how many devices to reimage.
         @param board: which kind of devices to reimage.
@@ -54,7 +68,7 @@
         """
         record('START', None, 'try new image')
         self._ensure_version_label(VERSION_PREFIX+name)
-        canary = self._schedule_reimage_job(url, name, num, board)
+        canary = self._schedule_reimage_job(name, num, board)
         logging.debug('Created re-imaging job: %d', canary.id)
         while len(self._afe.get_jobs(id=canary.id, not_yet_run=True)) > 0:
             time.sleep(10)
@@ -103,30 +117,27 @@
         return control_file + control_file_in
 
 
-    def _schedule_reimage_job(self, url, name, num_machines, board):
+    def _schedule_reimage_job(self, name, num_machines, board):
         """
         Schedules the reimaging of |num_machines| |board| devices with |image|.
 
         Sends an RPC to the autotest frontend to enqueue reimaging jobs on
         |num_machines| devices of type |board|
 
-        @param url: the URL of the image to install.
         @param name: the name of the image to install (must be unique).
-        @param num: how many devices to reimage.
+        @param num_machines: how many devices to reimage.
         @param board: which kind of devices to reimage.
         @return a frontend.Job object for the reimaging job we scheduled.
         """
         control_file = self._inject_vars(
-            { 'image_url': url,
+            { 'image_url': _image_url_pattern() % name,
               'image_name': name },
             self._cf_getter.get_control_file_contents_by_name('autoupdate'))
 
-        dargs = { 'control_file': control_file,
-                  'name': name + '-try',
-                  'control_type': 'Server',
-                  'meta_hosts': [board] * num_machines }
-
-        return self._afe.create_job(**dargs)
+        return self._afe.create_job(control_file=control_file,
+                                    name=name + '-try',
+                                    control_type='Server',
+                                    meta_hosts=[board] * num_machines)
 
 
     def _report_results(self, job, record):
diff --git a/server/cros/dynamic_suite_unittest.py b/server/cros/dynamic_suite_unittest.py
index 226657e..696ce31 100755
--- a/server/cros/dynamic_suite_unittest.py
+++ b/server/cros/dynamic_suite_unittest.py
@@ -34,7 +34,7 @@
     @var _BOARD: fake board to reimage
     """
 
-    _URL = 'http://nothing'
+    _URL = 'http://nothing/%s'
     _NAME = 'name'
     _NUM = 4
     _BOARD = 'board'
@@ -149,15 +149,18 @@
         cf_getter.get_control_file_contents_by_name('autoupdate').AndReturn('')
         self.reimager._cf_getter = cf_getter
 
+        # Fake out getting the image URL pattern.
+        self.mox.StubOutWithMock(dynamic_suite, '_image_url_pattern')
+        dynamic_suite._image_url_pattern().AndReturn(self._URL)
+
         self.afe.create_job(
             control_file=mox.And(mox.StrContains(self._NAME),
-                                 mox.StrContains(self._URL)),
+                                 mox.StrContains(self._URL % self._NAME)),
             name=mox.StrContains(self._NAME),
             control_type='Server',
             meta_hosts=[self._BOARD] * self._NUM)
         self.mox.ReplayAll()
-        self.reimager._schedule_reimage_job(self._URL, self._NAME,
-                                            self._NUM, self._BOARD)
+        self.reimager._schedule_reimage_job(self._NAME, self._NUM, self._BOARD)
 
 
     def expect_attempt(self, success):
@@ -171,8 +174,7 @@
         self.reimager._ensure_version_label(mox.StrContains(self._NAME))
 
         self.mox.StubOutWithMock(self.reimager, '_schedule_reimage_job')
-        self.reimager._schedule_reimage_job(self._URL,
-                                            self._NAME,
+        self.reimager._schedule_reimage_job(self._NAME,
                                             self._NUM,
                                             self._BOARD).AndReturn(canary)
         if success is not None:
@@ -194,8 +196,7 @@
         rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
         rjob.record('END GOOD', mox.IgnoreArg(), mox.IgnoreArg())
         self.mox.ReplayAll()
-        self.reimager.attempt(self._URL, self._NAME,
-                              self._NUM, self._BOARD, rjob.record)
+        self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
 
 
     def testFailedReimage(self):
@@ -206,8 +207,7 @@
         rjob.record('START', mox.IgnoreArg(), mox.IgnoreArg())
         rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
         self.mox.ReplayAll()
-        self.reimager.attempt(self._URL, self._NAME,
-                              self._NUM, self._BOARD, rjob.record)
+        self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
 
 
     def testReimageThatNeverHappened(self):
@@ -219,8 +219,7 @@
         rjob.record('FAIL', mox.IgnoreArg(), canary.name, mox.IgnoreArg())
         rjob.record('END FAIL', mox.IgnoreArg(), mox.IgnoreArg())
         self.mox.ReplayAll()
-        self.reimager.attempt(self._URL, self._NAME,
-                              self._NUM, self._BOARD, rjob.record)
+        self.reimager.attempt(self._NAME, self._NUM, self._BOARD, rjob.record)
 
 
 class SuiteTest(mox.MoxTestBase):
@@ -394,7 +393,7 @@
                 """Compares this object to a recorded status."""
                 return self._equals_record(*args)
 
-            def _equals_record(self, status, subdir, name, reason):
+            def _equals_record(self, status, subdir, name, reason=None):
                 """Compares this object and fields of recorded status."""
                 if 'aborted' in self.entry and self.entry['aborted']:
                     return status == 'ABORT'
diff --git a/server/site_server_job.py b/server/site_server_job.py
index be5208c..d796ff7 100644
--- a/server/site_server_job.py
+++ b/server/site_server_job.py
@@ -151,4 +151,3 @@
         self.record('START', None, skipped_test.test_name)
         self.record('INFO', None, skipped_test.test_name, msg)
         self.record('END TEST_NA', None, skipped_test.test_name, msg)
-
diff --git a/test_suites/control.bvt b/test_suites/control.bvt
index 385b470..abe47e6 100755
--- a/test_suites/control.bvt
+++ b/test_suites/control.bvt
@@ -15,24 +15,21 @@
 DOC = """
 This is the Build Verification Test suite.  It should consist of SHORT tests
 that validate critical functionality -- ability to acquire connectivity, perform
-crash reporting, get updates, and allow a user to log in, among other things..
+crash reporting, get updates, and allow a user to log in, among other things.
+
+@param image_name: The name of the image to test.
+                     Ex: x86-mario-r17/R17-1412.33.0-a1-b29
+@param board: The board to test on.  Ex: netbook_MARIO_MP
+@param SKIP_IMAGE: (optional) If present and True, don't re-image devices.
 """
 
 import common
 from autotest_lib.server.cros import dynamic_suite
 
-
-# These params should be injected by the thing scheduling the job
-image_url = 'http://172.22.50.205:8080/update/x86-mario-r17/R17-1388.0.0-a1-b1323'
-image_name ='x86-mario-r17/R17-1388.0.0-a1-b1323'
-board = 'netbook_MARIO_MP'
-
-# This is pretty much just here for testing.
-SKIP_IMAGE = True
-
 suite_tag = 'bvt'
 reimager = dynamic_suite.Reimager(job.autodir)
 
-if SKIP_IMAGE or reimager.attempt(image_url, image_name, 4, board, job.record):
+if (reimager.skip(globals()) or
+    reimager.attempt(image_name, 4, board, job.record)):
     bvt = dynamic_suite.Suite.create_from_name(suite_tag, job.autodir)
     bvt.run_and_wait(image_name, job.record, add_experimental=True)
diff --git a/test_suites/control.dummy b/test_suites/control.dummy
index e301614..dc4553f 100644
--- a/test_suites/control.dummy
+++ b/test_suites/control.dummy
@@ -20,18 +20,10 @@
 import common
 from autotest_lib.server.cros import dynamic_suite
 
-
-# These params should be injected by the thing scheduling the job
-image_url = 'http://172.22.50.205:8080/update/x86-mario-r17/R17-1388.0.0-a1-b1323'
-image_name ='x86-mario-r17/R17-1388.0.0-a1-b1323'
-board = 'netbook_MARIO_MP'
-
-# This is pretty much just here for testing.
-SKIP_IMAGE = False
-
 suite_tag = 'dummy'
 reimager = dynamic_suite.Reimager(job.autodir)
 
-if SKIP_IMAGE or reimager.attempt(image_url, image_name, 4, board, job.record):
+if (reimager.skip(globals()) or
+    reimager.attempt(image_name, 4, board, job.record)):
     bvt = dynamic_suite.Suite.create_from_name(suite_tag, job.autodir)
     bvt.run_and_wait(image_name, job.record, add_experimental=True)
diff --git a/test_suites/dev_harness b/test_suites/dev_harness
new file mode 100644
index 0000000..ad09d14
--- /dev/null
+++ b/test_suites/dev_harness
@@ -0,0 +1,14 @@
+# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+new_globals = globals()
+new_globals['image_name'] = 'x86-mario-r17/R17-1412.33.0-a1-b29'
+new_globals['board'] = 'netbook_MARIO_MP'
+new_globals['SKIP_IMAGE'] = True
+
+import os
+
+execfile(os.path.join(job.autodir, 'test_suites/control.dummy'),
+         new_globals,
+         locals())