# Copyright 2018 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. import json import logging import os from autotest_lib.client.common_lib import error from autotest_lib.server import test from autotest_lib.server import utils class tast_Runner(test.test): """Autotest server test that runs a Tast test suite. Tast is an integration-testing framework analagous to the test-running portion of Autotest. See https://chromium.googlesource.com/chromiumos/platform/tast/ for more information. This class runs the "tast" command locally to execute a Tast test suite on a remote DUT. """ version = 1 # Maximum time to wait for the tast command to complete, in seconds. _EXEC_TIMEOUT_SEC = 600 # JSON file written by the tast command containing test results. _RESULTS_FILENAME = 'results.json' # Maximum number of failing tests to include in error message. _MAX_TEST_NAMES_IN_ERROR = 3 # Default paths where Tast files are installed. _TAST_PATH = '/usr/bin/tast' _REMOTE_BUNDLE_DIR = '/usr/libexec/tast/bundles/remote' _REMOTE_DATA_DIR = '/usr/share/tast/data/remote' _REMOTE_TEST_RUNNER_PATH = '/usr/bin/remote_test_runner' # When Tast is deployed from CIPD packages in the lab, it's installed under # this prefix rather than under the root directory. _CIPD_INSTALL_ROOT = '/opt/infra-tools' def initialize(self, host, test_exprs): """ @param host: remote.RemoteHost instance representing DUT. @param test_exprs: Array of strings describing tests to run. @raises error.TestFail if the Tast installation couldn't be found. """ self._host = host self._test_exprs = test_exprs # The data dir can be missing if no remote tests registered data files, # but all other files must exist. self._tast_path = self._get_path(self._TAST_PATH) self._remote_bundle_dir = self._get_path(self._REMOTE_BUNDLE_DIR) self._remote_data_dir = self._get_path(self._REMOTE_DATA_DIR, allow_missing=True) self._remote_test_runner_path = self._get_path( self._REMOTE_TEST_RUNNER_PATH) def run_once(self): """Runs the test suite once.""" self._log_version() self._run_tast() self._parse_results() def _get_path(self, path, allow_missing=False): """Returns the path to an installed Tast-related file or directory. @param path Absolute path in root filesystem, e.g. "/usr/bin/tast". @param allow_missing True if it's okay for the path to be missing. @return: Absolute path within install root, e.g. "/opt/infra-tools/usr/bin/tast", or an empty string if the path wasn't found and allow_missing is True. @raises error.TestFail if the path couldn't be found and allow_missing is False. """ if os.path.exists(path): return path cipd_path = os.path.join(self._CIPD_INSTALL_ROOT, os.path.relpath(path, '/')) if os.path.exists(cipd_path): return cipd_path if allow_missing: return '' raise error.TestFail('Neither %s nor %s exists' % (path, cipd_path)) def _log_version(self): """Runs the tast command locally to log its version.""" try: utils.run([self._tast_path, '-version'], timeout=self._EXEC_TIMEOUT_SEC, stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, stderr_is_expected=True, stdout_level=logging.INFO, stderr_level=logging.ERROR) except error.CmdError as e: logging.error('Failed to log tast version: %s' % str(e)) def _run_tast(self): """Runs the tast command locally to perform testing against the DUT. @raises error.TestFail if the tast command fails or times out (but not if individual tests fail). """ cmd = [ self._tast_path, '-verbose', '-logtime=false', 'run', '-build=false', '-resultsdir=' + self.resultsdir, '-remotebundledir=' + self._remote_bundle_dir, '-remotedatadir=' + self._remote_data_dir, '-remoterunner=' + self._remote_test_runner_path, self._host.hostname, ] cmd.extend(self._test_exprs) logging.info('Running ' + ' '.join([utils.sh_quote_word(a) for a in cmd])) try: utils.run(cmd, ignore_status=False, timeout=self._EXEC_TIMEOUT_SEC, stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, stderr_is_expected=True, stdout_level=logging.INFO, stderr_level=logging.ERROR) except error.CmdError as e: raise error.TestFail('Failed to run tast: %s' % str(e)) except error.CmdTimeoutError as e: raise error.TestFail('Got timeout while running tast: %s' % str(e)) def _parse_results(self): """Parses results written by the tast command. @raises error.TestFail if one or more tests failed. """ path = os.path.join(self.resultsdir, self._RESULTS_FILENAME) failed = [] with open(path, 'r') as f: for test in json.load(f): if test['errors']: name = test['name'] for err in test['errors']: logging.warning('%s: %s', name, err['reason']) # TODO(derat): Report failures in flaky tests in some other # way. if 'flaky' not in test.get('attr', []): failed.append(name) if failed: msg = '%d failed: ' % len(failed) msg += ' '.join(sorted(failed)[:self._MAX_TEST_NAMES_IN_ERROR]) if len(failed) > self._MAX_TEST_NAMES_IN_ERROR: msg += ' ...' raise error.TestFail(msg)