1*9c5db199SXin Li# Copyright 2018 The Chromium OS Authors. All rights reserved. 2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 3*9c5db199SXin Li# found in the LICENSE file. 4*9c5db199SXin Li 5*9c5db199SXin Lifrom __future__ import absolute_import, division, print_function 6*9c5db199SXin Li 7*9c5db199SXin Liimport datetime 8*9c5db199SXin Liimport json 9*9c5db199SXin Liimport logging 10*9c5db199SXin Liimport os 11*9c5db199SXin Liimport shutil 12*9c5db199SXin Liimport socket 13*9c5db199SXin Liimport tempfile 14*9c5db199SXin Lifrom collections import OrderedDict 15*9c5db199SXin Li 16*9c5db199SXin Liimport dateutil.parser 17*9c5db199SXin Liimport six 18*9c5db199SXin Liimport yaml 19*9c5db199SXin Lifrom autotest_lib.client.common_lib import (base_job, config_vars, error) 20*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import dev_server, tpm_utils 21*9c5db199SXin Lifrom autotest_lib.server import test, utils 22*9c5db199SXin Lifrom autotest_lib.server.cros.network import wifi_test_context_manager 23*9c5db199SXin Lifrom autotest_lib.server.hosts import cros_host, servo_constants, servo_host 24*9c5db199SXin Lifrom autotest_lib.site_utils.rpm_control_system import rpm_constants 25*9c5db199SXin Lifrom autotest_lib.utils import labellib 26*9c5db199SXin Lifrom six.moves import urllib 27*9c5db199SXin Li 28*9c5db199SXin Li# A datetime.DateTime representing the Unix epoch in UTC. 29*9c5db199SXin Li_UNIX_EPOCH = dateutil.parser.parse('1970-01-01T00:00:00Z') 30*9c5db199SXin Li 31*9c5db199SXin Li# Keywords that are used in result json file. 32*9c5db199SXin Li_KEY_NAME = 'name' 33*9c5db199SXin Li_KEY_START = 'start' 34*9c5db199SXin Li_KEY_END = 'end' 35*9c5db199SXin Li_KEY_ERRORS = 'errors' 36*9c5db199SXin Li_KEY_SKIP_REASON = 'skipReason' 37*9c5db199SXin Li_KEY_REASON = 'reason' 38*9c5db199SXin Li_KEY_TIME = 'time' 39*9c5db199SXin Li_KEY_MISSING_REASON = 'missingReason' 40*9c5db199SXin Li 41*9c5db199SXin Li 42*9c5db199SXin Lidef split_arguments(args): 43*9c5db199SXin Li """Splits arguments into the autotest and tast variable assignments. 44*9c5db199SXin Li Use the results as command_args and varslist respectively. 45*9c5db199SXin Li 46*9c5db199SXin Li @param args: List of strings passed to test_that --args 47*9c5db199SXin Li 48*9c5db199SXin Li @returns Array of Tauto args, Array of TAST variable assignments. 49*9c5db199SXin Li """ 50*9c5db199SXin Li 51*9c5db199SXin Li auto_args = [] 52*9c5db199SXin Li tast_vars = [] 53*9c5db199SXin Li for a in args: 54*9c5db199SXin Li if a.startswith("tast."): 55*9c5db199SXin Li tast_vars.append(a[5:]) 56*9c5db199SXin Li else: 57*9c5db199SXin Li auto_args.append(a) 58*9c5db199SXin Li return auto_args, tast_vars 59*9c5db199SXin Li 60*9c5db199SXin Li 61*9c5db199SXin Lidef _encode_text(text): 62*9c5db199SXin Li """Takes an unicode string into utf-8 string 63*9c5db199SXin Li (bytes for python 2 and text for python 3). 64*9c5db199SXin Li """ 65*9c5db199SXin Li if six.PY2: 66*9c5db199SXin Li return text.encode('utf-8') 67*9c5db199SXin Li return text 68*9c5db199SXin Li 69*9c5db199SXin Li 70*9c5db199SXin Lidef _encode_json(j): 71*9c5db199SXin Li """Takes JSON object parsed by json.load() family, and encode each unicode 72*9c5db199SXin Li strings into str. 73*9c5db199SXin Li """ 74*9c5db199SXin Li if isinstance(j, six.text_type): 75*9c5db199SXin Li return _encode_text(j) 76*9c5db199SXin Li if isinstance(j, list): 77*9c5db199SXin Li return [_encode_json(x) for x in j] 78*9c5db199SXin Li if isinstance(j, dict): 79*9c5db199SXin Li return dict((_encode_json(k), _encode_json(v)) 80*9c5db199SXin Li for k, v in six.iteritems(j)) 81*9c5db199SXin Li return j 82*9c5db199SXin Li 83*9c5db199SXin Li 84*9c5db199SXin Lidef _dut_server_arg(command_args): 85*9c5db199SXin Li """Return dut_server arg out of command_args if found.""" 86*9c5db199SXin Li dut_server_arg = None 87*9c5db199SXin Li dut_server = None 88*9c5db199SXin Li for arg in command_args: 89*9c5db199SXin Li if 'dut_servers=' in arg: 90*9c5db199SXin Li dut_server_arg = arg 91*9c5db199SXin Li break 92*9c5db199SXin Li if not dut_server_arg: 93*9c5db199SXin Li return 94*9c5db199SXin Li dut_server = dut_server_arg.split('=')[1] 95*9c5db199SXin Li 96*9c5db199SXin Li # In case multiple dutservers are passed... 97*9c5db199SXin Li # e.g.: "dut_servers=localhost:1343,localhost:5678" 98*9c5db199SXin Li if ',' in dut_server: 99*9c5db199SXin Li # Currently only support the first, until we map dut:dut_server 100*9c5db199SXin Li dut_server = dut_server.split(',')[0] 101*9c5db199SXin Li 102*9c5db199SXin Li return dut_server 103*9c5db199SXin Li 104*9c5db199SXin Li 105*9c5db199SXin Liclass TastConfigError(error.AutotestError): 106*9c5db199SXin Li """Indicates a problem with configuration files.""" 107*9c5db199SXin Li 108*9c5db199SXin Li 109*9c5db199SXin Liclass tast(test.test): 110*9c5db199SXin Li """Autotest server test that runs a Tast test suite. 111*9c5db199SXin Li 112*9c5db199SXin Li Tast is an integration-testing framework analagous to the test-running 113*9c5db199SXin Li portion of Autotest. See 114*9c5db199SXin Li https://chromium.googlesource.com/chromiumos/platform/tast/ for more 115*9c5db199SXin Li information. 116*9c5db199SXin Li 117*9c5db199SXin Li This class runs the "tast" command locally to execute a Tast test suite on a 118*9c5db199SXin Li remote DUT. 119*9c5db199SXin Li """ 120*9c5db199SXin Li version = 1 121*9c5db199SXin Li 122*9c5db199SXin Li # Maximum time to wait for various tast commands to complete, in seconds. 123*9c5db199SXin Li _VERSION_TIMEOUT_SEC = 10 124*9c5db199SXin Li _DOWNLOAD_TIMEOUT_SEC = 120 125*9c5db199SXin Li _LIST_TIMEOUT_SEC = 30 126*9c5db199SXin Li _LIST_TRIES = 2 127*9c5db199SXin Li 128*9c5db199SXin Li # Additional time to add to the run timeout (e.g. for collecting crashes and 129*9c5db199SXin Li # logs). 130*9c5db199SXin Li _RUN_OVERHEAD_SEC = 20 131*9c5db199SXin Li # Additional time given to the run command to exit before it's killed. 132*9c5db199SXin Li _RUN_EXIT_SEC = 5 133*9c5db199SXin Li 134*9c5db199SXin Li # Number of times to retry SSH connection attempts to the DUT. 135*9c5db199SXin Li _SSH_CONNECT_RETRIES = 2 136*9c5db199SXin Li 137*9c5db199SXin Li # File written by the tast command containing test results, as 138*9c5db199SXin Li # newline-terminated JSON TestResult objects. 139*9c5db199SXin Li _STREAMED_RESULTS_FILENAME = 'streamed_results.jsonl' 140*9c5db199SXin Li 141*9c5db199SXin Li # Text file written by the tast command if a global error caused the test 142*9c5db199SXin Li # run to fail (e.g. SSH connection to DUT was lost). 143*9c5db199SXin Li _RUN_ERROR_FILENAME = 'run_error.txt' 144*9c5db199SXin Li 145*9c5db199SXin Li # Maximum number of failing and missing tests to include in error messages. 146*9c5db199SXin Li _MAX_TEST_NAMES_IN_ERROR = 3 147*9c5db199SXin Li 148*9c5db199SXin Li # Default paths where Tast files are installed by Portage packages. 149*9c5db199SXin Li _PORTAGE_TAST_PATH = '/usr/bin/tast' 150*9c5db199SXin Li 151*9c5db199SXin Li # Alternate locations for Tast files when using Server-Side Packaging. 152*9c5db199SXin Li # These files are installed from autotest_server_package.tar.bz2. 153*9c5db199SXin Li _SSP_ROOT = '/usr/local/tast' 154*9c5db199SXin Li _SSP_TAST_PATH = os.path.join(_SSP_ROOT, 'tast') 155*9c5db199SXin Li _SSP_REMOTE_BUNDLE_DIR = os.path.join(_SSP_ROOT, 'bundles/remote') 156*9c5db199SXin Li _SSP_REMOTE_DATA_DIR = os.path.join(_SSP_ROOT, 'data') 157*9c5db199SXin Li _SSP_REMOTE_TEST_RUNNER_PATH = os.path.join(_SSP_ROOT, 'remote_test_runner') 158*9c5db199SXin Li _SSP_DEFAULT_VARS_DIR_PATH = os.path.join(_SSP_ROOT, 'vars') 159*9c5db199SXin Li 160*9c5db199SXin Li _F20_CONTAINER_BREADCRUMB = '/usr/local/f20container' 161*9c5db199SXin Li # Prefix added to Tast test names when writing their results to TKO 162*9c5db199SXin Li # status.log files. 163*9c5db199SXin Li _TEST_NAME_PREFIX = 'tast.' 164*9c5db199SXin Li 165*9c5db199SXin Li # Prefixes of keyval keys recorded for missing tests. 166*9c5db199SXin Li _MISSING_TEST_KEYVAL_PREFIX = 'tast_missing_test.' 167*9c5db199SXin Li 168*9c5db199SXin Li # Job start/end TKO event status codes from base_client_job._rungroup in 169*9c5db199SXin Li # client/bin/job.py. 170*9c5db199SXin Li _JOB_STATUS_START = 'START' 171*9c5db199SXin Li _JOB_STATUS_END_GOOD = 'END GOOD' 172*9c5db199SXin Li _JOB_STATUS_END_FAIL = 'END FAIL' 173*9c5db199SXin Li _JOB_STATUS_END_NOSTATUS = 'END NOSTATUS' 174*9c5db199SXin Li _JOB_STATUS_END_SKIP = 'END TEST_NA' 175*9c5db199SXin Li 176*9c5db199SXin Li # In-job TKO event status codes from base_client_job._run_test_base in 177*9c5db199SXin Li # client/bin/job.py and client/common_lib/error.py. 178*9c5db199SXin Li _JOB_STATUS_GOOD = 'GOOD' 179*9c5db199SXin Li _JOB_STATUS_FAIL = 'FAIL' 180*9c5db199SXin Li _JOB_STATUS_NOSTATUS = 'NOSTATUS' 181*9c5db199SXin Li _JOB_STATUS_SKIP = 'TEST_NA' 182*9c5db199SXin Li 183*9c5db199SXin Li # Status reason used when an individual Tast test doesn't finish running. 184*9c5db199SXin Li _TEST_DID_NOT_FINISH_MSG = 'Test did not finish' 185*9c5db199SXin Li # Status reason used when an individual Tast test doesn't start running. 186*9c5db199SXin Li _TEST_DID_NOT_RUN_MSG = 'Test did not run' 187*9c5db199SXin Li 188*9c5db199SXin Li def initialize(self, 189*9c5db199SXin Li host, 190*9c5db199SXin Li test_exprs, 191*9c5db199SXin Li ignore_test_failures=False, 192*9c5db199SXin Li max_run_sec=3600, 193*9c5db199SXin Li command_args=[], 194*9c5db199SXin Li install_root='/', 195*9c5db199SXin Li ssp=None, 196*9c5db199SXin Li build=None, 197*9c5db199SXin Li build_bundle='cros', 198*9c5db199SXin Li run_private_tests=True, 199*9c5db199SXin Li varsfiles=[], 200*9c5db199SXin Li download_data_lazily=True, 201*9c5db199SXin Li clear_tpm=True, 202*9c5db199SXin Li totalshards=1, 203*9c5db199SXin Li shardindex=0, 204*9c5db199SXin Li companion_duts={}, 205*9c5db199SXin Li varslist=[], 206*9c5db199SXin Li maybemissingvars='', 207*9c5db199SXin Li use_camera_box=False, 208*9c5db199SXin Li vars_gs_path='', 209*9c5db199SXin Li retries=0, 210*9c5db199SXin Li ephemeraldevserver=None, 211*9c5db199SXin Li is_cft=False, 212*9c5db199SXin Li exclude_missing=False, 213*9c5db199SXin Li test_filter_files=[], 214*9c5db199SXin Li report_skipped=False): 215*9c5db199SXin Li """ 216*9c5db199SXin Li @param host: remote.RemoteHost instance representing DUT. 217*9c5db199SXin Li @param test_exprs: Array of strings describing tests to run. 218*9c5db199SXin Li @param ignore_test_failures: If False, this test will fail if individual 219*9c5db199SXin Li Tast tests report failure. If True, this test will only fail in 220*9c5db199SXin Li response to the tast command failing to run successfully. This 221*9c5db199SXin Li should generally be False when the test is running inline and True 222*9c5db199SXin Li when it's running asynchronously. 223*9c5db199SXin Li @param max_run_sec: Integer maximum running time for the "tast run" 224*9c5db199SXin Li command in seconds. 225*9c5db199SXin Li @param command_args: List of arguments passed on the command line via 226*9c5db199SXin Li test_that's --args flag, i.e. |args| in control file. 227*9c5db199SXin Li @param install_root: Root directory under which Tast binaries are 228*9c5db199SXin Li installed. Alternate values may be passed by unit tests. 229*9c5db199SXin Li @param ssp: Whether to use SSP files. Default is to auto-detect. 230*9c5db199SXin Li @param build: Whether to build test runners and test bundles. 231*9c5db199SXin Li Default is to build if and only if SSP is unavailable 232*9c5db199SXin Li (i.e. build = not ssp). 233*9c5db199SXin Li @param build_bundle: Test bundle name to build. Effective only when 234*9c5db199SXin Li build=True. 235*9c5db199SXin Li @param run_private_tests: Download and run private tests. Effective 236*9c5db199SXin Li only when build=False. When build=True, build_bundle can be 237*9c5db199SXin Li specified to build and run a private bundle. 238*9c5db199SXin Li @param varsfiles: list of names of yaml files containing variables set 239*9c5db199SXin Li in |-varsfile| arguments. 240*9c5db199SXin Li @param download_data_lazily: If True, external data files are downloaded 241*9c5db199SXin Li lazily between tests. If false, external data files are downloaded 242*9c5db199SXin Li in a batch before running tests. 243*9c5db199SXin Li @param clear_tpm: clear the TPM first before running the tast tests. 244*9c5db199SXin Li @param totalshards: Total number of shards. 245*9c5db199SXin Li @param shardindex: The shard index to be run. 246*9c5db199SXin Li @param companion_duts: A map of role to DUT name to tast run command as 247*9c5db199SXin Li |-companiondut| arguments. Each entry in the map will be formatted 248*9c5db199SXin Li as "role:dut" for each -companiondut argument. 249*9c5db199SXin Li @param varslist: list of strings to pass to tast run command as |-vars| 250*9c5db199SXin Li arguments. Each string should be formatted as "name=value". 251*9c5db199SXin Li @param maybemissingvars: a regex to pass to tast run command as 252*9c5db199SXin Li |-maybemissingvars| arguments. 253*9c5db199SXin Li @param vars_gs_path: gs path to load vars from. The vars are loaded 254*9c5db199SXin Li from gs in json format (key = value), then stored in a local 255*9c5db199SXin Li yaml file. The local file name is then appended to |-varsfiles|. 256*9c5db199SXin Li @param use_camera_box: Bring the IP address of chart device in CameraBox 257*9c5db199SXin Li to tast tests. 258*9c5db199SXin Li @param ephemeraldevserver: A value to pass to -ephemeraldevserver 259*9c5db199SXin Li @param exclude_missing: This option will exclude tests that are requested, but not found in 260*9c5db199SXin Li `tast list` command 261*9c5db199SXin Li @param test_filter_files: This option includes a list of files containing names 262*9c5db199SXin Li of test to be disabled. 263*9c5db199SXin Li @param report_skipped: If true then skipped tests will be reported in 264*9c5db199SXin Li the status.log 265*9c5db199SXin Li 266*9c5db199SXin Li When the F20 breadcrumb is detected, it is assumed we are running in 267*9c5db199SXin Li the F20 container, meaning we will force disable SSP (though the 268*9c5db199SXin Li SSP flag should be false in this case). The F20 container is fully 269*9c5db199SXin Li build versioned and matches the chroot paths, so we do not want to 270*9c5db199SXin Li take the SSP logic. 271*9c5db199SXin Li 272*9c5db199SXin Li @raises error.TestFail if the Tast installation couldn't be found. 273*9c5db199SXin Li """ 274*9c5db199SXin Li f20_container = False 275*9c5db199SXin Li if os.path.exists(self._F20_CONTAINER_BREADCRUMB): 276*9c5db199SXin Li ssp = False 277*9c5db199SXin Li f20_container = True 278*9c5db199SXin Li if ssp is None: 279*9c5db199SXin Li ssp = os.path.exists(self._SSP_TAST_PATH) 280*9c5db199SXin Li if build is None: 281*9c5db199SXin Li build = not ssp 282*9c5db199SXin Li 283*9c5db199SXin Li self._host = host 284*9c5db199SXin Li self._test_exprs = test_exprs 285*9c5db199SXin Li self._ignore_test_failures = ignore_test_failures 286*9c5db199SXin Li self._max_run_sec = max_run_sec 287*9c5db199SXin Li self._command_args = command_args 288*9c5db199SXin Li self._install_root = install_root 289*9c5db199SXin Li self._ssp = ssp 290*9c5db199SXin Li self._build = build 291*9c5db199SXin Li self._build_bundle = build_bundle 292*9c5db199SXin Li self._run_private_tests = run_private_tests 293*9c5db199SXin Li self._fake_now = None 294*9c5db199SXin Li self._varsfiles = varsfiles 295*9c5db199SXin Li self._varslist = varslist 296*9c5db199SXin Li self._download_data_lazily = download_data_lazily 297*9c5db199SXin Li self._clear_tpm = clear_tpm 298*9c5db199SXin Li self._totalshards = totalshards 299*9c5db199SXin Li self._shardindex = shardindex 300*9c5db199SXin Li self._companion_duts = companion_duts 301*9c5db199SXin Li self._maybemissingvars = maybemissingvars 302*9c5db199SXin Li self._vars_gs_path = vars_gs_path 303*9c5db199SXin Li self._use_camera_box = use_camera_box 304*9c5db199SXin Li self._retries = retries 305*9c5db199SXin Li self._f20_container = f20_container or is_cft 306*9c5db199SXin Li self._ephemeraldevserver = ephemeraldevserver 307*9c5db199SXin Li self._exclude_missing = exclude_missing 308*9c5db199SXin Li self._test_filter_files = test_filter_files 309*9c5db199SXin Li self._report_skipped = report_skipped 310*9c5db199SXin Li 311*9c5db199SXin Li # Need to pass in dut_servers for every test in CFT. 312*9c5db199SXin Li # But only add it if not already in varslist. 313*9c5db199SXin Li if not any( 314*9c5db199SXin Li [True if 'dut.servers' in arg else False for arg in varslist]): 315*9c5db199SXin Li dut_server = _dut_server_arg(command_args) 316*9c5db199SXin Li if dut_server: 317*9c5db199SXin Li self._varslist.append('servers.dut=:%s' % dut_server) 318*9c5db199SXin Li 319*9c5db199SXin Li # List of JSON objects describing tests that will be run. See Test in 320*9c5db199SXin Li # src/platform/tast/src/chromiumos/tast/testing/test.go for details. 321*9c5db199SXin Li self._tests_to_run = [] 322*9c5db199SXin Li 323*9c5db199SXin Li # List of JSON objects corresponding to tests from 324*9c5db199SXin Li # _STREAMED_RESULTS_FILENAME. See TestResult in 325*9c5db199SXin Li # src/platform/tast/src/chromiumos/cmd/tast/run/results.go for details. 326*9c5db199SXin Li self._test_results = [] 327*9c5db199SXin Li 328*9c5db199SXin Li # Error message read from _RUN_ERROR_FILENAME, if any. 329*9c5db199SXin Li self._run_error = None 330*9c5db199SXin Li 331*9c5db199SXin Li self._tast_path = self._get_path( 332*9c5db199SXin Li self._SSP_TAST_PATH if ssp else self._PORTAGE_TAST_PATH) 333*9c5db199SXin Li 334*9c5db199SXin Li # Register a hook to write the results of individual Tast tests as 335*9c5db199SXin Li # top-level entries in the TKO status.log file. 336*9c5db199SXin Li self.job.add_post_run_hook(self._log_all_unique_tests) 337*9c5db199SXin Li 338*9c5db199SXin Li def run_once(self): 339*9c5db199SXin Li """Runs a single iteration of the test.""" 340*9c5db199SXin Li 341*9c5db199SXin Li if self._clear_tpm: 342*9c5db199SXin Li tpm_utils.ClearTPMOwnerRequest(self._host, wait_for_ready=True) 343*9c5db199SXin Li 344*9c5db199SXin Li self._log_version() 345*9c5db199SXin Li if not self._f20_container: 346*9c5db199SXin Li self._find_devservers() 347*9c5db199SXin Li 348*9c5db199SXin Li self._ensure_bundles() 349*9c5db199SXin Li 350*9c5db199SXin Li # Shortcut if no test belongs to the specified test_exprs. 351*9c5db199SXin Li list_test_exception = None 352*9c5db199SXin Li has_tests_to_run = False 353*9c5db199SXin Li for i in range(self._LIST_TRIES): 354*9c5db199SXin Li try: 355*9c5db199SXin Li if i > 0: 356*9c5db199SXin Li logging.info('Retrying to get which tests to run') 357*9c5db199SXin Li has_tests_to_run = bool(self._get_tests_to_run()) 358*9c5db199SXin Li list_test_exception = None 359*9c5db199SXin Li break 360*9c5db199SXin Li except Exception as e: 361*9c5db199SXin Li list_test_exception = e 362*9c5db199SXin Li if list_test_exception: 363*9c5db199SXin Li raise error.TestFail('Failed to get list of tests to run: %s' % 364*9c5db199SXin Li str(list_test_exception)) 365*9c5db199SXin Li if not has_tests_to_run: 366*9c5db199SXin Li return 367*9c5db199SXin Li 368*9c5db199SXin Li # TODO(b/221333999): There are no devservers in CFT (F20), so this 369*9c5db199SXin Li # would likely error. Once full CFT is done tast.py will be deprecated 370*9c5db199SXin Li # and this won't be needed. 371*9c5db199SXin Li if not self._f20_container: 372*9c5db199SXin Li self._pull_varsfile_from_gs() 373*9c5db199SXin Li 374*9c5db199SXin Li run_failed = False 375*9c5db199SXin Li run_failed_msg = None 376*9c5db199SXin Li try: 377*9c5db199SXin Li self._run_tests() 378*9c5db199SXin Li except Exception as e: 379*9c5db199SXin Li run_failed = True 380*9c5db199SXin Li run_failed_msg = str(e).split('\n', 1)[0] 381*9c5db199SXin Li raise 382*9c5db199SXin Li finally: 383*9c5db199SXin Li self._read_run_error() 384*9c5db199SXin Li # Parse partial results even if the tast command didn't finish. 385*9c5db199SXin Li self._parse_results(run_failed, run_failed_msg) 386*9c5db199SXin Li 387*9c5db199SXin Li def set_fake_now_for_testing(self, now): 388*9c5db199SXin Li """Sets a fake timestamp to use in place of time.time() for unit tests. 389*9c5db199SXin Li 390*9c5db199SXin Li @param now Numeric timestamp as would be returned by time.time(). 391*9c5db199SXin Li """ 392*9c5db199SXin Li self._fake_now = now 393*9c5db199SXin Li 394*9c5db199SXin Li def _pull_varsfile_from_gs(self): 395*9c5db199SXin Li """Pulls varsfiles from GS, does dynamic values transformation, stores 396*9c5db199SXin Li it as a local file and appends the file name to varsfiles. 397*9c5db199SXin Li 398*9c5db199SXin Li Has to be called after _get_tests_to_run since it's using _tests_to_run. 399*9c5db199SXin Li 400*9c5db199SXin Li @param varsgspath Path to varsfiles in GS e.g. 401*9c5db199SXin Li 'config/perf_cuj/perf_cuj.config'. 402*9c5db199SXin Li 403*9c5db199SXin Li @raises TastConfigError for config errors. 404*9c5db199SXin Li """ 405*9c5db199SXin Li if not self._vars_gs_path: 406*9c5db199SXin Li return 407*9c5db199SXin Li 408*9c5db199SXin Li devservers = dev_server.ImageServer.get_available_devservers() 409*9c5db199SXin Li devserver_url = devservers[0][0] 410*9c5db199SXin Li if not devserver_url: 411*9c5db199SXin Li raise TastConfigError('No devserver_url') 412*9c5db199SXin Li 413*9c5db199SXin Li logging.info('Using devserver: %s', devserver_url) 414*9c5db199SXin Li labels = self._host.host_info_store.get().labels 415*9c5db199SXin Li build = labellib.LabelsMapping(labels).get(labellib.Key.CROS_VERSION) 416*9c5db199SXin Li if not build: 417*9c5db199SXin Li raise TastConfigError( 418*9c5db199SXin Li 'Not able to detect build, means not running on Moblab.') 419*9c5db199SXin Li 420*9c5db199SXin Li ds = dev_server.ImageServer(devserver_url) 421*9c5db199SXin Li gs_bucket = dev_server._get_image_storage_server() 422*9c5db199SXin Li if not gs_bucket: 423*9c5db199SXin Li raise TastConfigError('No image storage server gs bucket') 424*9c5db199SXin Li 425*9c5db199SXin Li config_path, config_file = os.path.split(self._vars_gs_path) 426*9c5db199SXin Li archive_url = os.path.join(gs_bucket, config_path.strip('/')) 427*9c5db199SXin Li logging.info('Staging configuration from %s.', gs_bucket) 428*9c5db199SXin Li try: 429*9c5db199SXin Li ds.stage_artifacts(build, 430*9c5db199SXin Li archive_url=archive_url, 431*9c5db199SXin Li files=[config_file]) 432*9c5db199SXin Li except Exception as e: 433*9c5db199SXin Li raise TastConfigError('Staging artifacts failed: %s', str(e)) 434*9c5db199SXin Li 435*9c5db199SXin Li logging.info('Parsing configuration from %s.', archive_url) 436*9c5db199SXin Li config_url = os.path.join(devserver_url, 'static', 437*9c5db199SXin Li self._vars_gs_path.strip('/')) 438*9c5db199SXin Li response = urllib.request.urlopen(config_url) 439*9c5db199SXin Li vars = json.loads(response.read()) 440*9c5db199SXin Li test_args = dict() 441*9c5db199SXin Li for key in vars: 442*9c5db199SXin Li test_args[key] = vars[key] 443*9c5db199SXin Li logging.info('Read %d values from remote configuration.', len(vars)) 444*9c5db199SXin Li 445*9c5db199SXin Li extvars = self._fill_config_extvars() 446*9c5db199SXin Li test_args = config_vars.TransformConfig(test_args, extvars) 447*9c5db199SXin Li 448*9c5db199SXin Li with tempfile.NamedTemporaryFile(suffix='.yaml', 449*9c5db199SXin Li delete=False) as temp_file: 450*9c5db199SXin Li yaml.dump(test_args, stream=temp_file, default_flow_style=False) 451*9c5db199SXin Li self._varsfiles.append(temp_file.name) 452*9c5db199SXin Li 453*9c5db199SXin Li def _fill_config_extvars(self): 454*9c5db199SXin Li """Fill in external variables map for conditional config processing. 455*9c5db199SXin Li 456*9c5db199SXin Li The sources used (in order of precedence low to high): 457*9c5db199SXin Li * --varsfiles. 458*9c5db199SXin Li * --varslist. 459*9c5db199SXin Li * list of tests to run. 460*9c5db199SXin Li * command_args: List of arguments passed on the command line via 461*9c5db199SXin Li test_that's --args flag, i.e. |args| in control file. 462*9c5db199SXin Li * DUT labels (with and without a value). 463*9c5db199SXin Li 464*9c5db199SXin Li @returns external variables map. 465*9c5db199SXin Li """ 466*9c5db199SXin Li # The latter overwrites the former. 467*9c5db199SXin Li extvars = {} 468*9c5db199SXin Li 469*9c5db199SXin Li # Load varsfiles 470*9c5db199SXin Li for varsfile in self._varsfiles: 471*9c5db199SXin Li with open(varsfile, 'r') as f: 472*9c5db199SXin Li for key, val in yaml.safe_load(f).items(): 473*9c5db199SXin Li if 'var:' + key in extvars: 474*9c5db199SXin Li logging.info('var:%s overwritten', key) 475*9c5db199SXin Li extvars['var:' + key] = val 476*9c5db199SXin Li 477*9c5db199SXin Li # Load vars 478*9c5db199SXin Li for var in self._varslist: 479*9c5db199SXin Li key, val = var.split('=', 1) 480*9c5db199SXin Li if 'var:' + key in extvars: 481*9c5db199SXin Li logging.info('var:%s overwritten', key) 482*9c5db199SXin Li extvars['var:' + key] = val 483*9c5db199SXin Li 484*9c5db199SXin Li # Load tests_to_run 485*9c5db199SXin Li extvars['tests:'] = '\n'.join(self._tests_to_run) 486*9c5db199SXin Li for test_to_run in self._tests_to_run: 487*9c5db199SXin Li extvars['test:' + test_to_run] = '' 488*9c5db199SXin Li 489*9c5db199SXin Li # Load command_args 490*9c5db199SXin Li extvars['args:'] = '\n'.join(self._command_args) 491*9c5db199SXin Li for key, val in utils.args_to_dict(self._command_args).items(): 492*9c5db199SXin Li extvars['arg:' + key] = val 493*9c5db199SXin Li for command_arg in self._command_args: 494*9c5db199SXin Li if '=' not in command_arg and ':' not in command_arg: 495*9c5db199SXin Li extvars['arg:' + command_arg] = '' 496*9c5db199SXin Li 497*9c5db199SXin Li # Load labels 498*9c5db199SXin Li labels = self._host.host_info_store.get().labels 499*9c5db199SXin Li extvars['labels:'] = '\n'.join(labels) 500*9c5db199SXin Li for label in labels: 501*9c5db199SXin Li key, val = (label.split(':', 1) + [''])[0:2] 502*9c5db199SXin Li extvars['label:' + key] = val 503*9c5db199SXin Li 504*9c5db199SXin Li return extvars 505*9c5db199SXin Li 506*9c5db199SXin Li def _get_path(self, path): 507*9c5db199SXin Li """Returns the path to an installed Tast-related file or directory. 508*9c5db199SXin Li 509*9c5db199SXin Li @param path: Absolute paths in root filesystem, e.g. "/usr/bin/tast". 510*9c5db199SXin Li 511*9c5db199SXin Li @returns Absolute path within install root, e.g. 512*9c5db199SXin Li "/usr/local/tast/usr/bin/tast". 513*9c5db199SXin Li """ 514*9c5db199SXin Li return os.path.join(self._install_root, os.path.relpath(path, '/')) 515*9c5db199SXin Li 516*9c5db199SXin Li def _get_camerabox_args(self): 517*9c5db199SXin Li """Gets camerabox-related arguments to pass to "tast run". 518*9c5db199SXin Li 519*9c5db199SXin Li @returns List of command-line flag strings that should be inserted in 520*9c5db199SXin Li the command line after "tast run". 521*9c5db199SXin Li """ 522*9c5db199SXin Li args = [] 523*9c5db199SXin Li if self._use_camera_box: 524*9c5db199SXin Li host_name = self._host.hostname 525*9c5db199SXin Li 526*9c5db199SXin Li # If host name is "FOO.BAR.BAR2", the chart host name should be 527*9c5db199SXin Li # "FOO-tablet.BAR.BAR2" 528*9c5db199SXin Li domains = host_name.split('.', 1) 529*9c5db199SXin Li domains[0] += '-tablet' 530*9c5db199SXin Li chart_host_name = '.'.join(domains) 531*9c5db199SXin Li try: 532*9c5db199SXin Li chart_ip = socket.gethostbyname(chart_host_name) 533*9c5db199SXin Li 534*9c5db199SXin Li # Check if the IP is pingable. 535*9c5db199SXin Li if os.system("ping -c 1 " + chart_ip) != 0: 536*9c5db199SXin Li logging.error('Failed to ping IP: %s.', chart_ip) 537*9c5db199SXin Li 538*9c5db199SXin Li args += ['-var=chart=' + chart_ip] 539*9c5db199SXin Li except socket.gaierror: 540*9c5db199SXin Li logging.exception('Failed to get IP: %s.', chart_host_name) 541*9c5db199SXin Li logging.info('Camerabox args: %s', args) 542*9c5db199SXin Li return args 543*9c5db199SXin Li 544*9c5db199SXin Li def _get_servo_args(self): 545*9c5db199SXin Li """Gets servo-related arguments to pass to "tast run". 546*9c5db199SXin Li 547*9c5db199SXin Li @returns List of command-line flag strings that should be inserted in 548*9c5db199SXin Li the command line after "tast run". 549*9c5db199SXin Li """ 550*9c5db199SXin Li # Start with information provided by the Autotest database. 551*9c5db199SXin Li merged_args = {} 552*9c5db199SXin Li host_args = servo_host.get_servo_args_for_host(self._host) 553*9c5db199SXin Li if host_args: 554*9c5db199SXin Li merged_args.update(host_args) 555*9c5db199SXin Li 556*9c5db199SXin Li # Incorporate information that was passed manually. 557*9c5db199SXin Li args_dict = utils.args_to_dict(self._command_args) 558*9c5db199SXin Li merged_args.update(cros_host.CrosHost.get_servo_arguments(args_dict)) 559*9c5db199SXin Li 560*9c5db199SXin Li logging.info('Autotest servo-related args: %s', merged_args) 561*9c5db199SXin Li host_arg = merged_args.get(servo_constants.SERVO_HOST_ATTR) 562*9c5db199SXin Li port_arg = merged_args.get(servo_constants.SERVO_PORT_ATTR) 563*9c5db199SXin Li if not host_arg or not port_arg: 564*9c5db199SXin Li return [] 565*9c5db199SXin Li return ['-var=servo=%s:%s' % (host_arg, port_arg)] 566*9c5db199SXin Li 567*9c5db199SXin Li def _get_firmware_args(self): 568*9c5db199SXin Li """Gets firmware-related arguments to pass to "tast run". 569*9c5db199SXin Li 570*9c5db199SXin Li @returns List of command-line flag strings that should be inserted in 571*9c5db199SXin Li the command line after "tast run". 572*9c5db199SXin Li """ 573*9c5db199SXin Li # Incorporate information that was passed manually. 574*9c5db199SXin Li args_dict = utils.args_to_dict(self._command_args) 575*9c5db199SXin Li 576*9c5db199SXin Li args = [] 577*9c5db199SXin Li no_ec_sync = args_dict.get("no_ec_sync") 578*9c5db199SXin Li if no_ec_sync: 579*9c5db199SXin Li args += ['-var=firmware.no_ec_sync=' + no_ec_sync] 580*9c5db199SXin Li logging.info('Firmware args: %s', args) 581*9c5db199SXin Li return args 582*9c5db199SXin Li 583*9c5db199SXin Li def _get_rpm_args(self): 584*9c5db199SXin Li """Gets rpm-related arguments to pass to "tast run". 585*9c5db199SXin Li 586*9c5db199SXin Li @returns List of command-line flag strings that should be inserted in 587*9c5db199SXin Li the command line after "tast run". 588*9c5db199SXin Li """ 589*9c5db199SXin Li info = self._host.host_info_store.get() 590*9c5db199SXin Li args = [] 591*9c5db199SXin Li forward_args = [ 592*9c5db199SXin Li (rpm_constants.POWERUNIT_HOSTNAME_KEY, 'powerunitHostname=%s'), 593*9c5db199SXin Li (rpm_constants.POWERUNIT_OUTLET_KEY, 'powerunitOutlet=%s'), 594*9c5db199SXin Li (rpm_constants.HYDRA_HOSTNAME_KEY, 'hydraHostname=%s'), 595*9c5db199SXin Li ] 596*9c5db199SXin Li for key, var_arg in forward_args: 597*9c5db199SXin Li if key in info.attributes: 598*9c5db199SXin Li args += ['-var=' + var_arg % info.attributes[key]] 599*9c5db199SXin Li logging.info('RPM args: %s', args) 600*9c5db199SXin Li return args 601*9c5db199SXin Li 602*9c5db199SXin Li def _get_wificell_args(self): 603*9c5db199SXin Li """Gets wificell-related (router, pcap) arguments to pass to "tast run". 604*9c5db199SXin Li 605*9c5db199SXin Li @returns List of command-line flag strings that should be inserted in 606*9c5db199SXin Li the command line after "tast run". 607*9c5db199SXin Li """ 608*9c5db199SXin Li # Incorporate information that was passed manually. 609*9c5db199SXin Li args_dict = utils.args_to_dict(self._command_args) 610*9c5db199SXin Li args = [] 611*9c5db199SXin Li # Alias of WiFiTestContextManager. 612*9c5db199SXin Li WiFiManager = wifi_test_context_manager.WiFiTestContextManager 613*9c5db199SXin Li # TODO(crbug.com/1065601): plumb other WiFi test specific arguments, 614*9c5db199SXin Li # e.g. pcap address. See: WiFiTestContextManager's constants. 615*9c5db199SXin Li forward_args = [ 616*9c5db199SXin Li (WiFiManager.CMDLINE_ROUTER_ADDR, 'router=%s'), 617*9c5db199SXin Li (WiFiManager.CMDLINE_PCAP_ADDR, 'pcap=%s'), 618*9c5db199SXin Li ] 619*9c5db199SXin Li for key, var_arg in forward_args: 620*9c5db199SXin Li if key in args_dict: 621*9c5db199SXin Li args += ['-var=' + var_arg % args_dict[key]] 622*9c5db199SXin Li # Append "routers" var for supporting multi-router tests with current 623*9c5db199SXin Li # two-AP fixture setup (with specified router_addr and pcap_addr args). 624*9c5db199SXin Li # TODO(b/171949862): remove this when a new multi-router fixture is 625*9c5db199SXin Li # defined and rolled out to the lab. 626*9c5db199SXin Li if (WiFiManager.CMDLINE_ROUTER_ADDR in args_dict 627*9c5db199SXin Li and WiFiManager.CMDLINE_PCAP_ADDR in args_dict): 628*9c5db199SXin Li args += [ 629*9c5db199SXin Li '-var=routers=%s,%s' % 630*9c5db199SXin Li (args_dict[WiFiManager.CMDLINE_ROUTER_ADDR], 631*9c5db199SXin Li args_dict[WiFiManager.CMDLINE_PCAP_ADDR]) 632*9c5db199SXin Li ] 633*9c5db199SXin Li logging.info('Autotest wificell-related args: %s', args) 634*9c5db199SXin Li return args 635*9c5db199SXin Li 636*9c5db199SXin Li def _get_cloud_storage_info(self): 637*9c5db199SXin Li """Gets the cloud storage bucket URL to pass to tast. 638*9c5db199SXin Li 639*9c5db199SXin Li @returns Cloud storage bucket URL that should be inserted in 640*9c5db199SXin Li the command line after "tast run". 641*9c5db199SXin Li """ 642*9c5db199SXin Li gs_bucket = dev_server._get_image_storage_server() 643*9c5db199SXin Li args_dict = utils.args_to_dict(self._command_args) 644*9c5db199SXin Li build = args_dict.get('build') 645*9c5db199SXin Li if not build: 646*9c5db199SXin Li labels = self._host.host_info_store.get().labels 647*9c5db199SXin Li build = labellib.LabelsMapping(labels).get( 648*9c5db199SXin Li labellib.Key.CROS_VERSION) 649*9c5db199SXin Li 650*9c5db199SXin Li if not gs_bucket or not build: 651*9c5db199SXin Li return [] 652*9c5db199SXin Li gs_path = os.path.join(gs_bucket, build) 653*9c5db199SXin Li if not gs_path.endswith('/'): 654*9c5db199SXin Li gs_path += '/' 655*9c5db199SXin Li logging.info('Cloud storage bucket: %s', gs_path) 656*9c5db199SXin Li return ['-buildartifactsurl=%s' % gs_path] 657*9c5db199SXin Li 658*9c5db199SXin Li def _find_devservers(self): 659*9c5db199SXin Li """Finds available devservers. 660*9c5db199SXin Li 661*9c5db199SXin Li The result is saved as self._devserver_args. 662*9c5db199SXin Li """ 663*9c5db199SXin Li logging.info('All devservers: %s', 664*9c5db199SXin Li ', '.join(dev_server.ImageServer.servers())) 665*9c5db199SXin Li devservers, can_retry = dev_server.ImageServer.get_available_devservers( 666*9c5db199SXin Li self._host.hostname, prefer_local_devserver=True) 667*9c5db199SXin Li if not devservers and can_retry and (self._host.is_satlab() 668*9c5db199SXin Li or 'MOBLAB' in os.environ): 669*9c5db199SXin Li devservers, can_retry = dev_server.ImageServer.get_available_devservers( 670*9c5db199SXin Li self._host.hostname, prefer_local_devserver=False) 671*9c5db199SXin Li logging.info('Using devservers: %s', ', '.join(devservers)) 672*9c5db199SXin Li self._devserver_args = ['-devservers=%s' % ','.join(devservers)] 673*9c5db199SXin Li if self._ephemeraldevserver is not None: 674*9c5db199SXin Li self._devserver_args.append('-ephemeraldevserver=%s' % 675*9c5db199SXin Li self._ephemeraldevserver) 676*9c5db199SXin Li 677*9c5db199SXin Li def _log_version(self): 678*9c5db199SXin Li """Runs the tast command locally to log its version.""" 679*9c5db199SXin Li try: 680*9c5db199SXin Li utils.run([self._tast_path, '-version'], 681*9c5db199SXin Li timeout=self._VERSION_TIMEOUT_SEC, 682*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, 683*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 684*9c5db199SXin Li stderr_is_expected=True, 685*9c5db199SXin Li stdout_level=logging.INFO, 686*9c5db199SXin Li stderr_level=logging.ERROR) 687*9c5db199SXin Li except error.CmdError as e: 688*9c5db199SXin Li logging.error('Failed to log tast version: %s', str(e)) 689*9c5db199SXin Li 690*9c5db199SXin Li def _run_tast(self, 691*9c5db199SXin Li subcommand, 692*9c5db199SXin Li extra_subcommand_args, 693*9c5db199SXin Li test_exprs, 694*9c5db199SXin Li timeout_sec, 695*9c5db199SXin Li log_stdout=False, 696*9c5db199SXin Li ignore_status=False): 697*9c5db199SXin Li """Runs the tast command locally to e.g. list available tests or perform 698*9c5db199SXin Li testing against the DUT. 699*9c5db199SXin Li 700*9c5db199SXin Li @param subcommand: Subcommand to pass to the tast executable, e.g. 'run' 701*9c5db199SXin Li or 'list'. 702*9c5db199SXin Li @param extra_subcommand_args: List of additional subcommand arguments. 703*9c5db199SXin Li @param test_exprs: Array of strings describing tests to run. 704*9c5db199SXin Li @param timeout_sec: Integer timeout for the command in seconds. 705*9c5db199SXin Li @param log_stdout: If true, write stdout to log. 706*9c5db199SXin Li @param ignore_status: If true, command execution errors are ignored. 707*9c5db199SXin Li 708*9c5db199SXin Li @returns client.common_lib.utils.CmdResult object describing the result. 709*9c5db199SXin Li 710*9c5db199SXin Li @raises error.TestFail if the tast command fails or times out. 711*9c5db199SXin Li """ 712*9c5db199SXin Li cmd = [ 713*9c5db199SXin Li self._tast_path, 714*9c5db199SXin Li '-verbose=true', 715*9c5db199SXin Li '-logtime=false', 716*9c5db199SXin Li subcommand, 717*9c5db199SXin Li '-sshretries=%d' % self._SSH_CONNECT_RETRIES, 718*9c5db199SXin Li '-downloaddata=%s' % ( 719*9c5db199SXin Li 'lazy' if self._download_data_lazily else 'batch'), 720*9c5db199SXin Li '-totalshards=%s' % self._totalshards, 721*9c5db199SXin Li '-shardindex=%s' % self._shardindex, 722*9c5db199SXin Li ] 723*9c5db199SXin Li if self._f20_container: 724*9c5db199SXin Li cmd.extend(['-build=false']) 725*9c5db199SXin Li if self._run_private_tests: 726*9c5db199SXin Li cmd.append('-downloadprivatebundles=true') 727*9c5db199SXin Li elif self._build: 728*9c5db199SXin Li cmd.extend([ 729*9c5db199SXin Li '-build=true', 730*9c5db199SXin Li '-buildbundle=%s' % self._build_bundle, 731*9c5db199SXin Li '-checkbuilddeps=false', 732*9c5db199SXin Li ]) 733*9c5db199SXin Li else: 734*9c5db199SXin Li cmd.append('-build=false') 735*9c5db199SXin Li if self._ssp: 736*9c5db199SXin Li remote_test_runner_path = self._get_path( 737*9c5db199SXin Li self._SSP_REMOTE_TEST_RUNNER_PATH) 738*9c5db199SXin Li if not os.path.exists(remote_test_runner_path): 739*9c5db199SXin Li raise error.TestFail( 740*9c5db199SXin Li '%s does not exist (broken SSP?)' % 741*9c5db199SXin Li remote_test_runner_path) 742*9c5db199SXin Li cmd.extend([ 743*9c5db199SXin Li '-remotebundledir=%s' % self._get_path( 744*9c5db199SXin Li self._SSP_REMOTE_BUNDLE_DIR), 745*9c5db199SXin Li '-remotedatadir=%s' % self._get_path( 746*9c5db199SXin Li self._SSP_REMOTE_DATA_DIR), 747*9c5db199SXin Li '-remoterunner=%s' % remote_test_runner_path, 748*9c5db199SXin Li ]) 749*9c5db199SXin Li if subcommand == 'run': 750*9c5db199SXin Li cmd.append('-defaultvarsdir=%s' % 751*9c5db199SXin Li self._get_path(self._SSP_DEFAULT_VARS_DIR_PATH)) 752*9c5db199SXin Li if self._run_private_tests: 753*9c5db199SXin Li cmd.append('-downloadprivatebundles=true') 754*9c5db199SXin Li if not self._f20_container: 755*9c5db199SXin Li cmd.extend(self._devserver_args) 756*9c5db199SXin Li cmd.extend(extra_subcommand_args) 757*9c5db199SXin Li cmd.append('%s%s' % (self._host.hostname, ':%d' % 758*9c5db199SXin Li self._host.port if self._host.port else '')) 759*9c5db199SXin Li cmd.extend(test_exprs) 760*9c5db199SXin Li 761*9c5db199SXin Li logging.info('Running %s', 762*9c5db199SXin Li ' '.join([utils.sh_quote_word(a) for a in cmd])) 763*9c5db199SXin Li try: 764*9c5db199SXin Li return utils.run( 765*9c5db199SXin Li cmd, 766*9c5db199SXin Li ignore_status=ignore_status, 767*9c5db199SXin Li timeout=timeout_sec, 768*9c5db199SXin Li stdout_tee=(utils.TEE_TO_LOGS if log_stdout else None), 769*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 770*9c5db199SXin Li stderr_is_expected=True, 771*9c5db199SXin Li stdout_level=logging.INFO, 772*9c5db199SXin Li stderr_level=logging.ERROR) 773*9c5db199SXin Li except error.CmdError as e: 774*9c5db199SXin Li # Run several commands to debug possible network issues. 775*9c5db199SXin Li # TODO(b/189332919): Remove this logic once we finish debugging. 776*9c5db199SXin Li logging.info('Tast exited abnormally. Running several commands to ' 777*9c5db199SXin Li 'diagnose possible network issues...') 778*9c5db199SXin Li utils.run('time getent ahosts %s' % self._host.hostname, 779*9c5db199SXin Li timeout=60, 780*9c5db199SXin Li ignore_status=True, 781*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, 782*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 783*9c5db199SXin Li stderr_is_expected=True, 784*9c5db199SXin Li stdout_level=logging.INFO, 785*9c5db199SXin Li stderr_level=logging.ERROR) 786*9c5db199SXin Li utils.run( 787*9c5db199SXin Li 'ssh ' 788*9c5db199SXin Li # Enable maximum debug logging. 789*9c5db199SXin Li '-vvv ' 790*9c5db199SXin Li # Disable connection sharing to debug connection issues. 791*9c5db199SXin Li '-o ControlPath=none ' 792*9c5db199SXin Li # Following arguments were copied from Autotest logs. 793*9c5db199SXin Li '-a -x ' 794*9c5db199SXin Li '-o StrictHostKeyChecking=no ' 795*9c5db199SXin Li '-o UserKnownHostsFile=/dev/null ' 796*9c5db199SXin Li '-o BatchMode=yes ' 797*9c5db199SXin Li '-o ConnectTimeout=10 ' 798*9c5db199SXin Li '-o ConnectionAttempts=3 ' 799*9c5db199SXin Li '-l root %s%s true' % 800*9c5db199SXin Li ('-p %d ' % self._host.port if self._host.port else '', 801*9c5db199SXin Li self._host.hostname), 802*9c5db199SXin Li timeout=60, 803*9c5db199SXin Li ignore_status=True, 804*9c5db199SXin Li stdout_tee=utils.TEE_TO_LOGS, 805*9c5db199SXin Li stderr_tee=utils.TEE_TO_LOGS, 806*9c5db199SXin Li stderr_is_expected=True, 807*9c5db199SXin Li stdout_level=logging.INFO, 808*9c5db199SXin Li stderr_level=logging.ERROR) 809*9c5db199SXin Li # The tast command's output generally ends with a line describing 810*9c5db199SXin Li # the error that was encountered; include it in the first line of 811*9c5db199SXin Li # the TestFail exception. Fall back to stderr if stdout is empty (as 812*9c5db199SXin Li # is the case with the "list" subcommand, which uses stdout to print 813*9c5db199SXin Li # test data). 814*9c5db199SXin Li get_last_line = lambda s: s.strip().split('\n')[-1].strip() 815*9c5db199SXin Li last_line = (get_last_line(e.result_obj.stdout) or 816*9c5db199SXin Li get_last_line(e.result_obj.stderr)) 817*9c5db199SXin Li msg = (' (last line: %s)' % last_line) if last_line else '' 818*9c5db199SXin Li raise error.TestFail('Failed to run tast%s: %s' % (msg, str(e))) 819*9c5db199SXin Li except error.CmdTimeoutError as e: 820*9c5db199SXin Li raise error.TestFail('Got timeout while running tast: %s' % str(e)) 821*9c5db199SXin Li 822*9c5db199SXin Li def _ensure_bundles(self): 823*9c5db199SXin Li """Runs the tast command to ensure all test bundles are available. 824*9c5db199SXin Li 825*9c5db199SXin Li If private test bundles are available, they are downloaded from cloud 826*9c5db199SXin Li storage and installed to the DUT. Otherwise it is no-nop. 827*9c5db199SXin Li 828*9c5db199SXin Li Note that "tast list" also downloads private test bundles if they are 829*9c5db199SXin Li missing. Nevertheless we attempt to download them in advance because 830*9c5db199SXin Li "tast list" cannot emit detailed logs due to technical limitations and 831*9c5db199SXin Li often make it difficult to debug issues related to private test bundle 832*9c5db199SXin Li installation. 833*9c5db199SXin Li """ 834*9c5db199SXin Li logging.info('Downloading private test bundles (if any)') 835*9c5db199SXin Li temp_dir = tempfile.mkdtemp() 836*9c5db199SXin Li try: 837*9c5db199SXin Li args = ['-resultsdir=' + temp_dir] + self._get_cloud_storage_info() 838*9c5db199SXin Li for role, dut in sorted(self._companion_duts.items()): 839*9c5db199SXin Li args.append('-companiondut=%s:%s' % (role, dut)) 840*9c5db199SXin Li 841*9c5db199SXin Li for var in self._varslist: 842*9c5db199SXin Li args.append('-var=%s' % var) 843*9c5db199SXin Li 844*9c5db199SXin Li for file_name in self._test_filter_files: 845*9c5db199SXin Li args.append('-testfilterfile=%s' % file_name) 846*9c5db199SXin Li 847*9c5db199SXin Li # Start "tast run" with an attribute expression matching no test 848*9c5db199SXin Li # to trigger a private test bundle download. 849*9c5db199SXin Li # Note that Tast CLI will exit abnormally when no test matches, 850*9c5db199SXin Li # so we set ignore_status=True to avoid throwing TestFail. 851*9c5db199SXin Li self._run_tast('run', 852*9c5db199SXin Li args, ['("group:none")'], 853*9c5db199SXin Li tast._DOWNLOAD_TIMEOUT_SEC, 854*9c5db199SXin Li log_stdout=True, 855*9c5db199SXin Li ignore_status=True) 856*9c5db199SXin Li finally: 857*9c5db199SXin Li shutil.rmtree(temp_dir, ignore_errors=True) 858*9c5db199SXin Li 859*9c5db199SXin Li def _get_tests_to_run(self): 860*9c5db199SXin Li """Runs the tast command to update the list of tests that will be run. 861*9c5db199SXin Li 862*9c5db199SXin Li @returns False if no tests matched by test_exprs; True otherwise 863*9c5db199SXin Li 864*9c5db199SXin Li @raises error.TestFail if the tast command fails or times out. 865*9c5db199SXin Li """ 866*9c5db199SXin Li logging.info('Getting list of tests that will be run') 867*9c5db199SXin Li args = ['-json=true'] + self._get_cloud_storage_info() 868*9c5db199SXin Li result = self._run_tast('list', args, self._test_exprs, 869*9c5db199SXin Li self._LIST_TIMEOUT_SEC) 870*9c5db199SXin Li try: 871*9c5db199SXin Li self._tests_to_run = _encode_json(json.loads( 872*9c5db199SXin Li result.stdout.strip())) 873*9c5db199SXin Li except ValueError as e: 874*9c5db199SXin Li raise error.TestFail('Failed to parse tests: %s' % str(e)) 875*9c5db199SXin Li if len(self._tests_to_run) == 0: 876*9c5db199SXin Li expr = ' '.join([utils.sh_quote_word(a) for a in self._test_exprs]) 877*9c5db199SXin Li logging.warning('No tests matched by %s', expr) 878*9c5db199SXin Li return False 879*9c5db199SXin Li 880*9c5db199SXin Li logging.info('Expect to run %d test(s)', len(self._tests_to_run)) 881*9c5db199SXin Li 882*9c5db199SXin Li logging.info('Tests in scope:') 883*9c5db199SXin Li for test in self._tests_to_run: 884*9c5db199SXin Li logging.info('Test: %s', test['name']) 885*9c5db199SXin Li 886*9c5db199SXin Li return True 887*9c5db199SXin Li 888*9c5db199SXin Li def _run_tests(self): 889*9c5db199SXin Li """Runs the tast command to perform testing. 890*9c5db199SXin Li 891*9c5db199SXin Li @raises error.TestFail if the tast command fails or times out (but not 892*9c5db199SXin Li if individual tests fail). 893*9c5db199SXin Li """ 894*9c5db199SXin Li args = [ 895*9c5db199SXin Li '-resultsdir=' + self.resultsdir, 896*9c5db199SXin Li '-waituntilready=true', 897*9c5db199SXin Li '-timeout=' + str(self._max_run_sec), 898*9c5db199SXin Li '-continueafterfailure=true', 899*9c5db199SXin Li ] 900*9c5db199SXin Li args.extend(self._get_servo_args()) 901*9c5db199SXin Li args.extend(self._get_rpm_args()) 902*9c5db199SXin Li args.extend(self._get_wificell_args()) 903*9c5db199SXin Li args.extend(self._get_cloud_storage_info()) 904*9c5db199SXin Li args.extend(self._get_firmware_args()) 905*9c5db199SXin Li args.extend(self._get_camerabox_args()) 906*9c5db199SXin Li if self._retries: 907*9c5db199SXin Li args.append('-retries=%d' % self._retries) 908*9c5db199SXin Li 909*9c5db199SXin Li for varsfile in self._varsfiles: 910*9c5db199SXin Li args.append('-varsfile=%s' % varsfile) 911*9c5db199SXin Li 912*9c5db199SXin Li for var in self._varslist: 913*9c5db199SXin Li args.append('-var=%s' % var) 914*9c5db199SXin Li 915*9c5db199SXin Li if self._maybemissingvars: 916*9c5db199SXin Li args.append('-maybemissingvars=%s' % self._maybemissingvars) 917*9c5db199SXin Li 918*9c5db199SXin Li for role, dut in sorted(self._companion_duts.items()): 919*9c5db199SXin Li args.append( 920*9c5db199SXin Li '-companiondut=%s:%s%s' % 921*9c5db199SXin Li (role, dut.hostname, ':%d' % dut.port if dut.port else '')) 922*9c5db199SXin Li 923*9c5db199SXin Li for file in self._test_filter_files: 924*9c5db199SXin Li args.append('-testfilterfile=%s' % file) 925*9c5db199SXin Li 926*9c5db199SXin Li logging.info('Running tests with timeout of %d sec', self._max_run_sec) 927*9c5db199SXin Li # This option will exclude tests that are requested, but not found in 928*9c5db199SXin Li # `tast list` command 929*9c5db199SXin Li if self._exclude_missing: 930*9c5db199SXin Li tests_to_run_list = [test["name"] for test in self._tests_to_run] 931*9c5db199SXin Li self._run_tast('run', 932*9c5db199SXin Li args, 933*9c5db199SXin Li tests_to_run_list, 934*9c5db199SXin Li self._max_run_sec + tast._RUN_EXIT_SEC, 935*9c5db199SXin Li log_stdout=True) 936*9c5db199SXin Li else: 937*9c5db199SXin Li self._run_tast('run', 938*9c5db199SXin Li args, 939*9c5db199SXin Li self._test_exprs, 940*9c5db199SXin Li self._max_run_sec + tast._RUN_EXIT_SEC, 941*9c5db199SXin Li log_stdout=True) 942*9c5db199SXin Li 943*9c5db199SXin Li def _read_run_error(self): 944*9c5db199SXin Li """Reads a global run error message written by the tast command.""" 945*9c5db199SXin Li # The file is only written if a run error occurred. 946*9c5db199SXin Li path = os.path.join(self.resultsdir, self._RUN_ERROR_FILENAME) 947*9c5db199SXin Li if os.path.exists(path): 948*9c5db199SXin Li with open(path, 'r') as f: 949*9c5db199SXin Li self._run_error = f.read().strip() 950*9c5db199SXin Li 951*9c5db199SXin Li def maybe_replace(self, test, failed): 952*9c5db199SXin Li """ Removes a test from the list of failed results 953*9c5db199SXin Li 954*9c5db199SXin Li @param test: Name of test to remove from failed list 955*9c5db199SXin Li @param failed: List of failed tests 956*9c5db199SXin Li """ 957*9c5db199SXin Li # Remove the result, will take & only count the second result. 958*9c5db199SXin Li if test[_KEY_NAME] in failed: 959*9c5db199SXin Li failed.remove(test[_KEY_NAME]) 960*9c5db199SXin Li 961*9c5db199SXin Li def _parse_results(self, ignore_missing_file, run_error_msg): 962*9c5db199SXin Li """Parses results written by the tast command. 963*9c5db199SXin Li 964*9c5db199SXin Li @param ignore_missing_file: If True, return without raising an exception 965*9c5db199SXin Li if the Tast results file is missing. This is used to avoid raising a 966*9c5db199SXin Li new error if there was already an earlier error while running the 967*9c5db199SXin Li tast process. 968*9c5db199SXin Li @param run_error_msg: The error message from Tast when there is an 969*9c5db199SXin Li error. It will be None if Tast encounters no errors. 970*9c5db199SXin Li 971*9c5db199SXin Li @raises error.TestFail if results file is missing and 972*9c5db199SXin Li ignore_missing_file is False, or one or more tests failed and 973*9c5db199SXin Li _ignore_test_failures is false. 974*9c5db199SXin Li """ 975*9c5db199SXin Li # The file may not exist if "tast run" failed to run. Tests that were 976*9c5db199SXin Li # seen from the earlier "tast list" command will be reported as having 977*9c5db199SXin Li # missing results. 978*9c5db199SXin Li path = os.path.join(self.resultsdir, self._STREAMED_RESULTS_FILENAME) 979*9c5db199SXin Li if not os.path.exists(path): 980*9c5db199SXin Li if ignore_missing_file: 981*9c5db199SXin Li return 982*9c5db199SXin Li raise error.TestFail('Results file %s not found' % path) 983*9c5db199SXin Li 984*9c5db199SXin Li failed = set() 985*9c5db199SXin Li seen_test_names = set() 986*9c5db199SXin Li with open(path, 'r') as f: 987*9c5db199SXin Li for line in f: 988*9c5db199SXin Li line = line.strip() 989*9c5db199SXin Li if not line: 990*9c5db199SXin Li continue 991*9c5db199SXin Li try: 992*9c5db199SXin Li test = _encode_json(json.loads(line)) 993*9c5db199SXin Li except ValueError as e: 994*9c5db199SXin Li raise error.TestFail('Failed to parse %s: %s' % (path, e)) 995*9c5db199SXin Li self._test_results.append(test) 996*9c5db199SXin Li if test[_KEY_NAME] in seen_test_names: 997*9c5db199SXin Li self.maybe_replace(test, failed) 998*9c5db199SXin Li 999*9c5db199SXin Li name = test[_KEY_NAME] 1000*9c5db199SXin Li seen_test_names.add(name) 1001*9c5db199SXin Li 1002*9c5db199SXin Li if test.get(_KEY_ERRORS): 1003*9c5db199SXin Li for err in test[_KEY_ERRORS]: 1004*9c5db199SXin Li logging.warning('%s: %s', name, err[_KEY_REASON]) 1005*9c5db199SXin Li failed.add(name) 1006*9c5db199SXin Li else: 1007*9c5db199SXin Li # The test will have a zero (i.e. 0001-01-01 00:00:00 UTC) 1008*9c5db199SXin Li # end time (preceding the Unix epoch) if it didn't report 1009*9c5db199SXin Li # completion. 1010*9c5db199SXin Li if _rfc3339_time_to_timestamp(test[_KEY_END]) <= 0: 1011*9c5db199SXin Li failed.add(name) 1012*9c5db199SXin Li 1013*9c5db199SXin Li missing = [ 1014*9c5db199SXin Li t[_KEY_NAME] for t in self._tests_to_run 1015*9c5db199SXin Li if t[_KEY_NAME] not in seen_test_names 1016*9c5db199SXin Li ] 1017*9c5db199SXin Li 1018*9c5db199SXin Li if missing: 1019*9c5db199SXin Li self._record_missing_tests(missing) 1020*9c5db199SXin Li time_str = '%sZ' % datetime.datetime.utcnow().isoformat() 1021*9c5db199SXin Li for name in missing: 1022*9c5db199SXin Li t = {} 1023*9c5db199SXin Li t[_KEY_NAME] = name 1024*9c5db199SXin Li t[_KEY_START] = time_str 1025*9c5db199SXin Li t[_KEY_END] = time_str 1026*9c5db199SXin Li if self._run_error: 1027*9c5db199SXin Li t[_KEY_MISSING_REASON] = '%s due to global error: %s' % ( 1028*9c5db199SXin Li self._TEST_DID_NOT_RUN_MSG, self._run_error) 1029*9c5db199SXin Li elif run_error_msg: 1030*9c5db199SXin Li t[_KEY_MISSING_REASON] = run_error_msg 1031*9c5db199SXin Li else: 1032*9c5db199SXin Li t[_KEY_MISSING_REASON] = self._TEST_DID_NOT_RUN_MSG 1033*9c5db199SXin Li self._test_results.append(t) 1034*9c5db199SXin Li 1035*9c5db199SXin Li failure_msg = self._get_failure_message(failed, missing) 1036*9c5db199SXin Li if failure_msg: 1037*9c5db199SXin Li raise error.TestFail(failure_msg) 1038*9c5db199SXin Li 1039*9c5db199SXin Li def _get_failure_message(self, failed, missing): 1040*9c5db199SXin Li """Returns an error message describing failed and/or missing tests. 1041*9c5db199SXin Li 1042*9c5db199SXin Li @param failed: List of string names of Tast tests that failed. 1043*9c5db199SXin Li @param missing: List of string names of Tast tests with missing results. 1044*9c5db199SXin Li 1045*9c5db199SXin Li @returns String to be used as error.TestFail message. 1046*9c5db199SXin Li """ 1047*9c5db199SXin Li def list_tests(names): 1048*9c5db199SXin Li """Returns a string listing tests. 1049*9c5db199SXin Li 1050*9c5db199SXin Li @param names: List of string test names. 1051*9c5db199SXin Li 1052*9c5db199SXin Li @returns String listing tests. 1053*9c5db199SXin Li """ 1054*9c5db199SXin Li s = ' '.join(sorted(names)[:self._MAX_TEST_NAMES_IN_ERROR]) 1055*9c5db199SXin Li if len(names) > self._MAX_TEST_NAMES_IN_ERROR: 1056*9c5db199SXin Li s += ' ...' 1057*9c5db199SXin Li return s 1058*9c5db199SXin Li 1059*9c5db199SXin Li msg = '' 1060*9c5db199SXin Li if failed and not self._ignore_test_failures: 1061*9c5db199SXin Li msg = '%d failed: %s' % (len(failed), list_tests(failed)) 1062*9c5db199SXin Li if missing: 1063*9c5db199SXin Li if msg: 1064*9c5db199SXin Li msg += '; ' 1065*9c5db199SXin Li msg += '%d missing: %s' % (len(missing), list_tests(missing)) 1066*9c5db199SXin Li return msg 1067*9c5db199SXin Li 1068*9c5db199SXin Li def _log_all_unique_tests(self): 1069*9c5db199SXin Li """Writes entries to the TKO status.log file describing the results of 1070*9c5db199SXin Li all tests. 1071*9c5db199SXin Li 1072*9c5db199SXin Li If there are 2 tests with the same name, AND it has an error (failure) 1073*9c5db199SXin Li replace the result. 1074*9c5db199SXin Li Because: if it has an err AND a second result, its either: 1075*9c5db199SXin Li The first attempt is logged and failed and we want to use the 1076*9c5db199SXin Li retry result 1077*9c5db199SXin Li Or the attempts are out of order, and the already logged attempt is 1078*9c5db199SXin Li the second attempt which failed, meaning the first ALSO failed. 1079*9c5db199SXin Li So in this case, its still safe to override because we just 1080*9c5db199SXin Li need to mark the failure. 1081*9c5db199SXin Li The benefit of this is, if the first result is logged and failed, the 1082*9c5db199SXin Li retry might have passed, so we want to log that. 1083*9c5db199SXin Li 1084*9c5db199SXin Li """ 1085*9c5db199SXin Li seen_test_names = set() 1086*9c5db199SXin Li tests_to_log = OrderedDict() 1087*9c5db199SXin Li for test_res in self._test_results: 1088*9c5db199SXin Li test_name = test_res[_KEY_NAME] 1089*9c5db199SXin Li 1090*9c5db199SXin Li dup_res = tests_to_log.get(test_name) 1091*9c5db199SXin Li if not dup_res or dup_res.get(_KEY_ERRORS): 1092*9c5db199SXin Li tests_to_log[test_name] = test_res 1093*9c5db199SXin Li for test in tests_to_log.values(): 1094*9c5db199SXin Li self._log_test(test) 1095*9c5db199SXin Li seen_test_names.add(test[_KEY_NAME]) 1096*9c5db199SXin Li 1097*9c5db199SXin Li def _log_test(self, test): 1098*9c5db199SXin Li """Writes events to the TKO status.log file describing the results from 1099*9c5db199SXin Li a Tast test. 1100*9c5db199SXin Li 1101*9c5db199SXin Li @param test: A JSON object corresponding to a single test from a Tast 1102*9c5db199SXin Li results.json file. See TestResult in 1103*9c5db199SXin Li src/platform/tast/src/chromiumos/cmd/tast/run/results.go for 1104*9c5db199SXin Li details. 1105*9c5db199SXin Li """ 1106*9c5db199SXin Li name = test[_KEY_NAME] 1107*9c5db199SXin Li start_time = _rfc3339_time_to_timestamp(test[_KEY_START]) 1108*9c5db199SXin Li end_time = _rfc3339_time_to_timestamp(test[_KEY_END]) 1109*9c5db199SXin Li 1110*9c5db199SXin Li test_reported_errors = bool(test.get(_KEY_ERRORS)) 1111*9c5db199SXin Li test_skipped = bool(test.get(_KEY_SKIP_REASON)) 1112*9c5db199SXin Li test_not_run = bool(test.get(_KEY_MISSING_REASON)) 1113*9c5db199SXin Li # The test will have a zero (i.e. 0001-01-01 00:00:00 UTC) end time 1114*9c5db199SXin Li # (preceding the Unix epoch) if it didn't report completion. 1115*9c5db199SXin Li test_finished = end_time > 0 1116*9c5db199SXin Li 1117*9c5db199SXin Li # Avoid reporting tests that were skipped. 1118*9c5db199SXin Li if test_skipped and not test_reported_errors and not self._report_skipped: 1119*9c5db199SXin Li return 1120*9c5db199SXin Li 1121*9c5db199SXin Li # Look for magic error _TEST_DID_NOT_RUN_MSG and mark test as not run. 1122*9c5db199SXin Li for err in test.get(_KEY_ERRORS) or []: 1123*9c5db199SXin Li if err[_KEY_REASON] == self._TEST_DID_NOT_RUN_MSG: 1124*9c5db199SXin Li test_not_run = True 1125*9c5db199SXin Li test[_KEY_MISSING_REASON] = self._TEST_DID_NOT_RUN_MSG 1126*9c5db199SXin Li 1127*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_START, name, start_time) 1128*9c5db199SXin Li 1129*9c5db199SXin Li if test_not_run: 1130*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_NOSTATUS, name, end_time, 1131*9c5db199SXin Li test[_KEY_MISSING_REASON]) 1132*9c5db199SXin Li end_status = self._JOB_STATUS_END_NOSTATUS 1133*9c5db199SXin Li elif test_skipped and not test_reported_errors and self._report_skipped: 1134*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_SKIP, name, end_time, 1135*9c5db199SXin Li test.get(_KEY_SKIP_REASON)) 1136*9c5db199SXin Li end_status = self._JOB_STATUS_END_SKIP 1137*9c5db199SXin Li elif test_finished and not test_reported_errors: 1138*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_GOOD, name, end_time) 1139*9c5db199SXin Li end_status = self._JOB_STATUS_END_GOOD 1140*9c5db199SXin Li else: 1141*9c5db199SXin Li # The previous START event automatically increases the log 1142*9c5db199SXin Li # indentation level until the following END event. 1143*9c5db199SXin Li if test_reported_errors: 1144*9c5db199SXin Li for err in test[_KEY_ERRORS]: 1145*9c5db199SXin Li error_time = _rfc3339_time_to_timestamp(err[_KEY_TIME]) 1146*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_FAIL, name, 1147*9c5db199SXin Li error_time, err[_KEY_REASON]) 1148*9c5db199SXin Li if not test_finished: 1149*9c5db199SXin Li # If a run-level error was encountered (e.g. the SSH connection 1150*9c5db199SXin Li # to the DUT was lost), report it here to make it easier to see 1151*9c5db199SXin Li # the reason why the test didn't finish. 1152*9c5db199SXin Li if self._run_error: 1153*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_FAIL, name, 1154*9c5db199SXin Li start_time, self._run_error) 1155*9c5db199SXin Li self._log_test_event(self._JOB_STATUS_FAIL, name, start_time, 1156*9c5db199SXin Li self._TEST_DID_NOT_FINISH_MSG) 1157*9c5db199SXin Li end_time = start_time 1158*9c5db199SXin Li 1159*9c5db199SXin Li end_status = self._JOB_STATUS_END_FAIL 1160*9c5db199SXin Li 1161*9c5db199SXin Li self._log_test_event(end_status, name, end_time) 1162*9c5db199SXin Li 1163*9c5db199SXin Li def _log_test_event(self, status_code, test_name, timestamp, message=''): 1164*9c5db199SXin Li """Logs a single event to the TKO status.log file. 1165*9c5db199SXin Li 1166*9c5db199SXin Li @param status_code: Event status code, e.g. 'END GOOD'. See 1167*9c5db199SXin Li client/common_lib/log.py for accepted values. 1168*9c5db199SXin Li @param test_name: Tast test name, e.g. 'ui.ChromeLogin'. 1169*9c5db199SXin Li @param timestamp: Event timestamp (as seconds since Unix epoch). 1170*9c5db199SXin Li @param message: Optional human-readable message. 1171*9c5db199SXin Li """ 1172*9c5db199SXin Li full_name = self._TEST_NAME_PREFIX + test_name 1173*9c5db199SXin Li # The TKO parser code chokes on floating-point timestamps. 1174*9c5db199SXin Li entry = base_job.status_log_entry(status_code, 1175*9c5db199SXin Li None, 1176*9c5db199SXin Li full_name, 1177*9c5db199SXin Li message, 1178*9c5db199SXin Li None, 1179*9c5db199SXin Li timestamp=int(timestamp)) 1180*9c5db199SXin Li self.job.record_entry(entry, False) 1181*9c5db199SXin Li 1182*9c5db199SXin Li def _record_missing_tests(self, missing): 1183*9c5db199SXin Li """Records tests with missing results in job keyval file. 1184*9c5db199SXin Li 1185*9c5db199SXin Li @param missing: List of string names of Tast tests with missing results. 1186*9c5db199SXin Li """ 1187*9c5db199SXin Li keyvals = {} 1188*9c5db199SXin Li for i, name in enumerate(sorted(missing)): 1189*9c5db199SXin Li keyvals['%s%d' % (self._MISSING_TEST_KEYVAL_PREFIX, i)] = name 1190*9c5db199SXin Li utils.write_keyval(self.job.resultdir, keyvals) 1191*9c5db199SXin Li 1192*9c5db199SXin Li 1193*9c5db199SXin Liclass _LessBrokenParserInfo(dateutil.parser.parserinfo): 1194*9c5db199SXin Li """dateutil.parser.parserinfo that interprets years before 100 correctly. 1195*9c5db199SXin Li 1196*9c5db199SXin Li Our version of dateutil.parser.parse misinteprets an unambiguous string like 1197*9c5db199SXin Li '0001-01-01T00:00:00Z' as having a two-digit year, which it then converts to 1198*9c5db199SXin Li 2001. This appears to have been fixed by 1199*9c5db199SXin Li https://github.com/dateutil/dateutil/commit/fc696254. This parserinfo 1200*9c5db199SXin Li implementation always honors the provided year to prevent this from 1201*9c5db199SXin Li happening. 1202*9c5db199SXin Li """ 1203*9c5db199SXin Li def convertyear(self, year, century_specified=False): 1204*9c5db199SXin Li """Overrides convertyear in dateutil.parser.parserinfo.""" 1205*9c5db199SXin Li return int(year) 1206*9c5db199SXin Li 1207*9c5db199SXin Li 1208*9c5db199SXin Lidef _rfc3339_time_to_timestamp(time_str): 1209*9c5db199SXin Li """Converts an RFC3339 time into a Unix timestamp. 1210*9c5db199SXin Li 1211*9c5db199SXin Li @param time_str: RFC3339-compatible time, e.g. 1212*9c5db199SXin Li '2018-02-25T07:45:35.916929332-07:00'. 1213*9c5db199SXin Li 1214*9c5db199SXin Li @returns Float number of seconds since the Unix epoch. Negative if the time 1215*9c5db199SXin Li precedes the epoch. 1216*9c5db199SXin Li """ 1217*9c5db199SXin Li dt = dateutil.parser.parse(time_str, parserinfo=_LessBrokenParserInfo()) 1218*9c5db199SXin Li return (dt - _UNIX_EPOCH).total_seconds() 1219