[autotest] suite_scheduler.py should accept, honor 'num' in Task definitions

The suite scheduler needs to allow a sharding factor (num) to be
specified in Task stanzas, and pass it through to create_suite_job()
if specified.

BUG=chromium-os:33194
TEST=unit
TEST=create a suite_scheduler config that provides a 'num' field for
TEST=at least one task.  While running, watch the RPC log of the AFE
TEST=to ensure that your desired num is getting passed, but that
TEST=None is getting passed for other Tasks.
STATUS=Fixed

Change-Id: I049d76a835900df0c58502f40334a5b4c6c93401
Reviewed-on: https://gerrit.chromium.org/gerrit/29810
Commit-Ready: Chris Masone <cmasone@chromium.org>
Reviewed-by: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
diff --git a/site_utils/suite_scheduler/task.py b/site_utils/suite_scheduler/task.py
index b0c2456..64cc613 100644
--- a/site_utils/suite_scheduler/task.py
+++ b/site_utils/suite_scheduler/task.py
@@ -45,6 +45,7 @@
         run_on: event_on which to run  # Required
         branch_specs: factory,firmware,>=R12  # Optional
         pool: pool_of_devices  # Optional
+        num: sharding_factor  # int, Optional
 
         By default, Tasks run on all release branches, not factory or firmware.
 
@@ -56,7 +57,7 @@
         if not config.has_section(section):
             raise MalformedConfigEntry('unknown section %s' % section)
 
-        allowed = set(['suite', 'run_on', 'branch_specs', 'pool'])
+        allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num'])
         # The parameter of union() is the keys under the section in the config
         # The union merges this with the allowed set, so if any optional keys
         # are omitted, then they're filled in. If any extra keys are present,
@@ -71,6 +72,10 @@
         suite = config.getstring(section, 'suite')
         branches = config.getstring(section, 'branch_specs')
         pool = config.getstring(section, 'pool')
+        try:
+            num = config.getint(section, 'num')
+        except ValueError as e:
+            raise MalformedConfigEntry("Ill-specified 'num': %r" %e)
         if not keyword:
             raise MalformedConfigEntry('No event to |run_on|.')
         if not suite:
@@ -79,7 +84,7 @@
         if branches:
             specs = re.split('\s*,\s*', branches)
             Task.CheckBranchSpecs(specs)
-        return keyword, Task(section, suite, specs, pool)
+        return keyword, Task(section, suite, specs, pool, num)
 
 
     @staticmethod
@@ -103,7 +108,7 @@
             raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
 
 
-    def __init__(self, name, suite, branch_specs, pool=None):
+    def __init__(self, name, suite, branch_specs, pool=None, num=None):
         """Constructor
 
         Given an iterable in |branch_specs|, pre-vetted using CheckBranchSpecs,
@@ -124,16 +129,20 @@
           t._FitsSpec('firmware')  # False
           t._FitsSpec('goober')  # False
 
+        @param name: name of this task, e.g. 'NightlyPower'
         @param suite: the name of the suite to run, e.g. 'bvt'
         @param branch_specs: a pre-vetted iterable of branch specifiers,
                              e.g. ['>=R18', 'factory']
         @param pool: the pool of machines to use for scheduling purposes.
                      Default: None
+        @param num: the number of devices across which to shard the test suite.
+                    Default: None
         """
         self._name = name
         self._suite = suite
         self._branch_specs = branch_specs
         self._pool = pool
+        self._num = '%d' % num if num else None
 
         self._bare_branches = []
         if not branch_specs:
@@ -150,8 +159,8 @@
         # Since we expect __hash__() and other comparitor methods to be used
         # frequently by set operations, and they use str() a lot, pre-compute
         # the string representation of this object.
-        self._str = '%s: %s on %s with pool %s' % (self.__class__.__name__,
-                                                   suite, branch_specs, pool)
+        self._str = '%s: %s on %s with pool %s, across %r machines' % (
+            self.__class__.__name__, suite, branch_specs, pool, num)
 
 
     def _FitsSpec(self, branch):
@@ -189,6 +198,10 @@
         return self._str
 
 
+    def __repr__(self):
+        return self._str
+
+
     def __lt__(self, other):
         return str(self) < str(other)
 
@@ -272,7 +285,7 @@
         for build in builds:
             try:
                 if not scheduler.ScheduleSuite(self._suite, board, build,
-                                               self._pool, force):
+                                               self._pool, self._num, force):
                     logging.info('Skipping scheduling %s on %s for %s',
                                  self._suite, build, board)
             except deduping_scheduler.DedupingSchedulerException as e: