Add require_run functionality to test list.

BUG=None
TEST=goofy_unittest.py, manual

Change-Id: I26f478805a5e8ce060ebc9162a9ab5feb8b0b854
Reviewed-on: https://gerrit.chromium.org/gerrit/26664
Commit-Ready: Jon Salz <jsalz@chromium.org>
Reviewed-by: Jon Salz <jsalz@chromium.org>
Tested-by: Jon Salz <jsalz@chromium.org>
diff --git a/py/goofy/goofy.py b/py/goofy/goofy.py
index 65d1a11..c36f3b5 100755
--- a/py/goofy/goofy.py
+++ b/py/goofy/goofy.py
@@ -509,6 +509,13 @@
         self.tests_to_run.popleft()
         return
 
+      for i in test.require_run:
+        for j in i.walk():
+          if j.get_state().status == TestState.ACTIVE:
+            logging.info('Waiting for active test %s to complete '
+                   'before running %s', j.path, test.path)
+            return
+
       if self.invocations and not (test.backgroundable and all(
         [x.backgroundable for x in self.invocations])):
         logging.debug('Waiting for non-backgroundable tests to '
@@ -517,12 +524,43 @@
 
       self.tests_to_run.popleft()
 
+      untested = set()
+      for i in test.require_run:
+        for j in i.walk():
+          if j == test:
+            # We've hit this test itself; stop checking
+            break
+          if j.get_state().status == TestState.UNTESTED:
+            # Found an untested test; move on to the next
+            # element in require_run.
+            untested.add(j)
+            break
+
+      if untested:
+        untested_paths = ', '.join(sorted([x.path for x in untested]))
+        if self.state_instance.get_shared_data('engineering_mode',
+                                               optional=True):
+          # In engineering mode, we'll let it go.
+          factory.console.warn('In engineering mode; running '
+                               '%s even though required tests '
+                               '[%s] have not completed',
+                               test.path, untested_paths)
+        else:
+          # Not in engineering mode; mark it failed.
+          error_msg = ('Required tests [%s] have not been run yet'
+                       % untested_paths)
+          factory.console.error('Not running %s: %s',
+                                test.path, error_msg)
+          test.update_state(status=TestState.FAILED,
+                            error_msg=error_msg)
+          continue
+
       if isinstance(test, factory.ShutdownStep):
         if os.path.exists(NO_REBOOT_FILE):
           test.update_state(
             status=TestState.FAILED, increment_count=1,
             error_msg=('Skipped shutdown since %s is present' %
-                   NO_REBOOT_FILE))
+                       NO_REBOOT_FILE))
           continue
 
         test.update_state(status=TestState.ACTIVE, increment_count=1,
diff --git a/py/goofy/goofy_unittest.py b/py/goofy/goofy_unittest.py
index c0243dd..ce1f13c 100755
--- a/py/goofy/goofy_unittest.py
+++ b/py/goofy/goofy_unittest.py
@@ -120,7 +120,8 @@
   def before_init_goofy(self):
     '''Hook invoked before init_goofy.'''
 
-  def check_one_test(self, test_id, name, passed, error_msg, trigger=None):
+  def check_one_test(self, test_id, name, passed, error_msg, trigger=None,
+                     does_not_start=False):
     '''Runs a single autotest, waiting for it to complete.
 
     Args:
@@ -131,19 +132,22 @@
       trigger: An optional callable that will be executed after mocks are
         set up to trigger the autotest.  If None, then the test is
         expected to start itself.
+      does_not_start: If True, checks that the test does not start
+        (e.g., due to an unsatisfied require_run).
     '''
-    mock_autotest(self.env, name, passed, error_msg)
+    if not does_not_start:
+      mock_autotest(self.env, name, passed, error_msg)
     self.mocker.ReplayAll()
     if trigger:
       trigger()
     self.assertTrue(self.goofy.run_once())
-    self.assertEqual([test_id],
-             [test.path for test in self.goofy.invocations])
+    self.assertEqual([] if does_not_start else [test_id],
+                     [test.path for test in self.goofy.invocations])
     self._wait()
     test_state = self.state.get_test_state(test_id)
     self.assertEqual(TestState.PASSED if passed else TestState.FAILED,
              test_state.status)
-    self.assertEqual(1, test_state.count)
+    self.assertEqual(0 if does_not_start else 1, test_state.count)
     self.assertEqual(error_msg, test_state.error_msg)
 
 
@@ -425,6 +429,26 @@
     self.check_one_test('c', 'c_C', True, '')
 
 
+class RequireRunTest(GoofyTest):
+  options = '''
+    options.auto_run_on_start = False
+  '''
+  test_list = '''
+    OperatorTest(id='a', autotest_name='a_A'),
+    OperatorTest(id='b', autotest_name='b_B', require_run='a'),
+  '''
+  def runTest(self):
+    self.goofy.restart_tests(
+      root=self.goofy.test_list.lookup_path('b'))
+    self.check_one_test('b', 'b_B', False,
+              'Required tests [a] have not been run yet',
+              does_not_start=True)
+
+    self.goofy.restart_tests()
+    self.check_one_test('a', 'a_A', True, '')
+    self.check_one_test('b', 'b_B', True, '')
+
+
 if __name__ == "__main__":
   factory.init_logging('goofy_unittest')
   goofy._inited_logging = True
diff --git a/py/goofy/js/goofy.js b/py/goofy/js/goofy.js
index fdefadd..dff56fb 100644
--- a/py/goofy/js/goofy.js
+++ b/py/goofy/js/goofy.js
@@ -710,6 +710,7 @@
 cros.factory.Goofy.prototype.setEngineeringMode = function(enabled) {
     this.engineeringMode = enabled;
     goog.dom.classes.enable(document.body, 'goofy-engineering-mode', enabled);
+    this.sendRpc('set_shared_data', ['engineering_mode', enabled]);
 };
 
 /**
diff --git a/py/test/factory.py b/py/test/factory.py
index dd3b638..32c68d2 100644
--- a/py/test/factory.py
+++ b/py/test/factory.py
@@ -39,6 +39,10 @@
 # (for tests like '3G').
 ID_REGEXP = re.compile(r'^\w+$')
 
+# Special value for require_run meaning "all tests".
+ALL = 'all'
+
+
 class TestListError(Exception):
   pass
 
@@ -302,7 +306,8 @@
 
   # SHA1 hash for a eng password in UI. Use None to always
   # enable eng mode. To generate, run `echo -n '<password>'
-  # | sha1sum`.
+  # | sha1sum`. For example, for 'test0000', the hash is
+  # 266abb9bec3aff5c37bd025463ee5c14ac18bfca.
   engineering_password_sha1 = None
   _types['engineering_password_sha1'] = (type(None), str)
 
@@ -452,6 +457,7 @@
                has_ui=None,
                never_fails=None,
                exclusive=None,
+               require_run=None,
                _root=None,
                _default_id=None):
     '''
@@ -479,6 +485,14 @@
       May be a list or a single string. Items must all be in
       EXCLUSIVE_OPTIONS. Tests may not be backgroundable.
     @param _default_id: A default ID to use if no ID is specified.
+    @param require_run: Path (or list of paths) to tests that must have been
+      completed before this test may be run. If the specified path
+      includes this test, then all tests up to (but not including) this
+      test must have been run already. For instance, if this test is
+      SMT.FlushEventLogs, and require_run is "SMT", then all tests in
+      SMT before FlushEventLogs must have already been run. The string
+      "all" may be used to refer to the root (i.e., all tests in the
+      whole test list before this one must already have been run).
     @param _root: True only if this is the root node (for internal use
       only).
     '''
@@ -494,6 +508,10 @@
       self.exclusive = [exclusive]
     else:
       self.exclusive = exclusive or []
+    if isinstance(require_run, str):
+      self.require_run_paths = [require_run]
+    else:
+      self.require_run_paths = require_run or []
     self.subtests = subtests or []
     self.path = ''
     self.parent = None
@@ -746,6 +764,18 @@
     self.options = options
     self._init('', self.path_map)
 
+    # Resolve require_run_paths to the actual test objects.
+    for test in self.walk():
+      test.require_run = []
+      for x in test.require_run_paths:
+        required_test = self if x == ALL else self.lookup_path(x)
+        if not required_test:
+          raise TestListError(
+            "Unknown test %s in %s's require_run argument (note "
+            "that full paths are required)"
+            % (x, test.path))
+        test.require_run.append(required_test)
+
   def get_all_tests(self):
     '''
     Returns all FactoryTest objects.