[autotest] Handle config error in suite scheduler.

This change adds more error handling in parsing a task. If a task has
bad config values, log the error and ignore the task.

BUG=chromium:729330
TEST=unittest

Change-Id: Idfc068781211f4535dec5f2debbee671dc13ccc0
Reviewed-on: https://chromium-review.googlesource.com/531755
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Xixuan Wu <xixuan@chromium.org>
diff --git a/site_utils/suite_scheduler/driver.py b/site_utils/suite_scheduler/driver.py
index 3dcc964..861bd7f 100644
--- a/site_utils/suite_scheduler/driver.py
+++ b/site_utils/suite_scheduler/driver.py
@@ -8,6 +8,7 @@
 from multiprocessing import pool
 
 import base_event, board_enumerator, build_event, deduping_scheduler
+import error
 import task, timed_event
 
 import common
@@ -90,7 +91,7 @@
 
         for option in config.options(BOARD_WHITELIST_SECTION):
             if option in board_lists:
-                raise task.MalformedConfigEntry(
+                raise error.MalformedConfigEntry(
                         'Board list name must be unique.')
             else:
                 board_lists[option] = config.getstring(
@@ -140,7 +141,7 @@
                 try:
                     keyword, new_task = task.Task.CreateFromConfigSection(
                             config, section, board_lists=board_lists)
-                except task.MalformedConfigEntry as e:
+                except error.MalformedConfigEntry as e:
                     logging.warning('%s is malformed: %s', section, str(e))
                     continue
                 tasks.setdefault(keyword, []).append(new_task)
diff --git a/site_utils/suite_scheduler/error.py b/site_utils/suite_scheduler/error.py
new file mode 100644
index 0000000..e1c9ffa
--- /dev/null
+++ b/site_utils/suite_scheduler/error.py
@@ -0,0 +1,9 @@
+# Copyright 2017 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.
+
+"""Errors being raised in suite scheduler."""
+
+class MalformedConfigEntry(Exception):
+    """Raised to indicate a failure to parse a Task out of a config."""
+    pass
\ No newline at end of file
diff --git a/site_utils/suite_scheduler/forgiving_config_parser.py b/site_utils/suite_scheduler/forgiving_config_parser.py
index 07827f7..126ab48 100644
--- a/site_utils/suite_scheduler/forgiving_config_parser.py
+++ b/site_utils/suite_scheduler/forgiving_config_parser.py
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import ConfigParser
+import error
 
 
 def forgive_config_error(func):
@@ -12,6 +13,8 @@
             return func(*args, **kwargs)
         except ConfigParser.Error:
             return None
+        except ValueError as e:
+            raise error.MalformedConfigEntry(str(e))
     return wrapper
 
 
diff --git a/site_utils/suite_scheduler/task.py b/site_utils/suite_scheduler/task.py
index 1478db2..d181523 100644
--- a/site_utils/suite_scheduler/task.py
+++ b/site_utils/suite_scheduler/task.py
@@ -10,6 +10,7 @@
 import base_event
 import deduping_scheduler
 import driver
+import error
 import manifest_versions
 from distutils import version
 from constants import Labels
@@ -36,11 +37,6 @@
 # there is only one board specified in `boards`
 TESTBED_DUT_COUNT_REGEX = '[^,]*-(\d+)'
 
-class MalformedConfigEntry(Exception):
-    """Raised to indicate a failure to parse a Task out of a config."""
-    pass
-
-
 BARE_BRANCHES = ['factory', 'firmware']
 
 
@@ -124,7 +120,7 @@
         tot_spec = tot_spec.lower()
         match = re.match('(tot)[-]?(1$|2$)?', tot_spec)
         if not match:
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     "%s isn't a valid branch spec." % tot_spec)
         tot_mstone = self.tot
         num_back = match.groups()[1]
@@ -179,7 +175,7 @@
         @raise MalformedConfigEntry if there's a problem parsing |section|.
         """
         if not config.has_section(section):
-            raise MalformedConfigEntry('unknown section %s' % section)
+            raise error.MalformedConfigEntry('unknown section %s' % section)
 
         allowed = set(['suite', 'run_on', 'branch_specs', 'pool', 'num',
                        'boards', 'file_bugs', 'cros_build_spec',
@@ -193,7 +189,7 @@
         # comparison against the allowed set.
         section_headers = allowed.union(dict(config.items(section)).keys())
         if allowed != section_headers:
-            raise MalformedConfigEntry('unknown entries: %s' %
+            raise error.MalformedConfigEntry('unknown entries: %s' %
                       ", ".join(map(str, section_headers.difference(allowed))))
 
         keyword = config.getstring(section, 'run_on')
@@ -222,20 +218,20 @@
         try:
             num = config.getint(section, 'num')
         except ValueError as e:
-            raise MalformedConfigEntry("Ill-specified 'num': %r" % e)
+            raise error.MalformedConfigEntry("Ill-specified 'num': %r" % e)
         if not keyword:
-            raise MalformedConfigEntry('No event to |run_on|.')
+            raise error.MalformedConfigEntry('No event to |run_on|.')
         if not suite:
-            raise MalformedConfigEntry('No |suite|')
+            raise error.MalformedConfigEntry('No |suite|')
         try:
             hour = config.getint(section, 'hour')
         except ValueError as e:
-            raise MalformedConfigEntry("Ill-specified 'hour': %r" % e)
+            raise error.MalformedConfigEntry("Ill-specified 'hour': %r" % e)
         if hour is not None and (hour < 0 or hour > 23):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`hour` must be an integer between 0 and 23.')
         if hour is not None and keyword != 'nightly':
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`hour` is the trigger time that can only apply to nightly '
                     'event.')
 
@@ -248,13 +244,13 @@
         try:
             day = config.getint(section, 'day')
         except ValueError as e:
-            raise MalformedConfigEntry("Ill-specified 'day': %r" % e)
+            raise error.MalformedConfigEntry("Ill-specified 'day': %r" % e)
         if day is not None and (day < 0 or day > 6):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`day` must be an integer between 0 and 6, where 0 is for '
                     'Monday and 6 is for Sunday.')
         if day is not None and keyword != 'weekly':
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`day` is the trigger of the day of a week, that can only '
                     'apply to weekly events.')
 
@@ -265,27 +261,28 @@
 
         os_type = config.getstring(section, 'os_type') or OS_TYPE_CROS
         if os_type not in OS_TYPES:
-            raise MalformedConfigEntry('`os_type` must be one of %s' % OS_TYPES)
+            raise error.MalformedConfigEntry(
+                    '`os_type` must be one of %s' % OS_TYPES)
 
         lc_branches = config.getstring(section, 'branches')
         lc_targets = config.getstring(section, 'targets')
         if os_type == OS_TYPE_CROS and (lc_branches or lc_targets):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`branches` and `targets` are only supported for Launch '
                     'Control builds, not ChromeOS builds.')
         if (os_type in OS_TYPES_LAUNCH_CONTROL and
             (not lc_branches or not lc_targets)):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`branches` and `targets` must be specified for Launch '
                     'Control builds.')
         if (os_type in OS_TYPES_LAUNCH_CONTROL and boards and
             not testbed_dut_count):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     '`boards` for Launch Control builds are retrieved from '
                     '`targets` setting, it should not be set for Launch '
                     'Control builds.')
         if os_type == OS_TYPE_CROS and testbed_dut_count:
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     'testbed_dut_count is only supported for Launch Control '
                     'builds testing with testbed.')
 
@@ -353,7 +350,8 @@
                             branch[branch.index('tot'):])
                     have_seen_numeric_constraint = True
                 continue
-            raise MalformedConfigEntry("%s isn't a valid branch spec." % branch)
+            raise error.MalformedConfigEntry(
+                    "%s isn't a valid branch spec.'" % branch)
 
 
     def __init__(self, name, suite, branch_specs, pool=None, num=None,
@@ -483,11 +481,11 @@
              cros_build_spec) and
             not self.test_source in [Builds.FIRMWARE_RW, Builds.FIRMWARE_RO,
                                      Builds.CROS]):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     'You must specify the build for test source. It can only '
                     'be `firmware_rw`, `firmware_ro` or `cros`.')
         if self._firmware_rw_build_spec and cros_build_spec:
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     'You cannot specify both firmware_rw_build_spec and '
                     'cros_build_spec. firmware_rw_build_spec is used to specify'
                     ' a firmware build when the suite requires firmware to be '
@@ -496,12 +494,12 @@
                     'build when build_specs is set to firmware.')
         if (self._firmware_rw_build_spec and
             self._firmware_rw_build_spec not in ['firmware', 'cros']):
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     'firmware_rw_build_spec can only be empty, firmware or '
                     'cros. It does not support other build type yet.')
 
         if os_type not in OS_TYPES_LAUNCH_CONTROL and self._testbed_dut_count:
-            raise MalformedConfigEntry(
+            raise error.MalformedConfigEntry(
                     'testbed_dut_count is only applicable to testbed to run '
                     'test with builds from Launch Control.')
 
diff --git a/site_utils/suite_scheduler/task_unittest.py b/site_utils/suite_scheduler/task_unittest.py
index 0ffa047..d4158e6 100644
--- a/site_utils/suite_scheduler/task_unittest.py
+++ b/site_utils/suite_scheduler/task_unittest.py
@@ -10,6 +10,7 @@
 
 # driver must be imported first due to circular imports in base_event and task
 import driver  # pylint: disable-msg=W0611
+import error
 import deduping_scheduler, forgiving_config_parser, task, build_event
 
 
@@ -189,7 +190,7 @@
     def testCreateFromNoSuiteConfig(self):
         """Ensure we require a suite in Task config."""
         self.config.remove_option(self._TASK_NAME, 'suite')
-        self.assertRaises(task.MalformedConfigEntry,
+        self.assertRaises(error.MalformedConfigEntry,
                           task.Task.CreateFromConfigSection,
                           self.config,
                           self._TASK_NAME)
@@ -198,7 +199,7 @@
     def testCreateFromNoKeywordConfig(self):
         """Ensure we require a run_on event in Task config."""
         self.config.remove_option(self._TASK_NAME, 'run_on')
-        self.assertRaises(task.MalformedConfigEntry,
+        self.assertRaises(error.MalformedConfigEntry,
                           task.Task.CreateFromConfigSection,
                           self.config,
                           self._TASK_NAME)
@@ -206,7 +207,7 @@
 
     def testCreateFromNonexistentConfig(self):
         """Ensure we fail gracefully if we pass in a bad section name."""
-        self.assertRaises(task.MalformedConfigEntry,
+        self.assertRaises(error.MalformedConfigEntry,
                           task.Task.CreateFromConfigSection,
                           self.config,
                           'not_a_thing')
@@ -216,7 +217,7 @@
         """Ensure testbed_dut_count specified in boards is only applicable for
         testing Launch Control builds."""
         self.config.set(self._TASK_NAME, 'boards', 'shamu-2')
-        self.assertRaises(task.MalformedConfigEntry,
+        self.assertRaises(error.MalformedConfigEntry,
                           task.Task.CreateFromConfigSection,
                           self.config,
                           self._TASK_NAME)