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.