Allow UI startup to proceed even if test list is broken.

Display the error in a dialog in the UI.

BUG=None
TEST=Manual

Change-Id: Ib93e5403f04ee18f2aade0a93656c9b4808906c9
Reviewed-on: https://gerrit.chromium.org/gerrit/42852
Tested-by: Jon Salz <jsalz@chromium.org>
Reviewed-by: Cheng-Yi Chiang <cychiang@chromium.org>
Commit-Queue: Jon Salz <jsalz@chromium.org>
diff --git a/py/goofy/goofy.py b/py/goofy/goofy.py
index db622a8..de2d580 100755
--- a/py/goofy/goofy.py
+++ b/py/goofy/goofy.py
@@ -1070,16 +1070,36 @@
       self.state_instance.get_shared_data('shutdown_time', optional=True))
     self.state_instance.del_shared_data('shutdown_time', optional=True)
 
+    self.state_instance.del_shared_data('startup_error', optional=True)
     if not self.options.test_list:
       self.options.test_list = find_test_list()
-      if not self.options.test_list:
-        logging.error('No test list. Aborting.')
-        sys.exit(1)
+    if self.options.test_list:
       logging.info('Using test list %s', self.options.test_list)
+      try:
+        self.test_list = factory.read_test_list(
+            self.options.test_list,
+            self.state_instance)
+      except:  # pylint: disable=W0702
+        logging.exception('Unable to read test list %r', self.options.test_list)
+        self.state_instance.set_shared_data('startup_error',
+            'Unable to read test list %s\n%s' % (
+                self.options.test_list,
+                traceback.format_exc()))
+    else:
+      logging.error('No test list found.')
+      self.state_instance.set_shared_data('startup_error',
+                                          'No test list found.')
 
-    self.test_list = factory.read_test_list(
-      self.options.test_list,
-      self.state_instance)
+    if not self.test_list:
+      if self.options.ui == 'chrome':
+        # Create an empty test list with default options so that the rest of
+        # startup can proceed.
+        self.test_list = factory.FactoryTestList(
+            [], self.state_instance, factory.Options())
+      else:
+        # Bail with an error; no point in starting up.
+        sys.exit('No valid test list; exiting.')
+
     if not self.state_instance.has_shared_data('ui_lang'):
       self.state_instance.set_shared_data('ui_lang',
                         self.test_list.options.ui_lang)
diff --git a/py/goofy/js/goofy.js b/py/goofy/js/goofy.js
index 8550d6b..7e51189 100644
--- a/py/goofy/js/goofy.js
+++ b/py/goofy/js/goofy.js
@@ -725,6 +725,23 @@
                     this.engineeringPasswordSHA1 != null);
                 this.setEngineeringMode(this.engineeringPasswordSHA1 == null);
             });
+    this.sendRpc(
+        'get_shared_data', ['startup_error'],
+        function(error) {
+            this.alert(
+                cros.factory.Label(
+                    ('An error occurred while starting ' +
+                     'the factory test system.<br>' +
+                     'Factory testing cannot proceed.'),
+                    ('开工厂测试系统时发生错误.<br>' +
+                     '没办法继续测试.')) +
+                    '<div class="goofy-startup-error">' +
+                    goog.string.htmlEscape(error) +
+                    '</div>');
+        },
+        function() {
+            // Unable to retrieve the key; that's fine, no startup error!
+        });
 
     var timer = new goog.Timer(1000);
     goog.events.listen(timer, goog.Timer.TICK, this.updateTime, false, this);
@@ -889,7 +906,6 @@
     dialog.setContent(messageHtml);
     dialog.setVisible(true);
     goog.dom.classes.add(dialog.getElement(), 'goofy-alert');
-    this.positionOverConsole(dialog.getElement());
 };
 
 /**
diff --git a/py/goofy/static/goofy.css b/py/goofy/static/goofy.css
index b2cca2b..332f40a 100644
--- a/py/goofy/static/goofy.css
+++ b/py/goofy/static/goofy.css
@@ -410,6 +410,10 @@
   margin-top: 0.5em;
   width: 100%;
 }
+.goofy-startup-error {
+  white-space: pre;
+  margin-top: 1em;
+}
 .modal-dialog-buttons {
   text-align: center;
 }