1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2013 The Chromium Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li 7*9c5db199SXin Lifrom __future__ import absolute_import 8*9c5db199SXin Lifrom __future__ import division 9*9c5db199SXin Lifrom __future__ import print_function 10*9c5db199SXin Li 11*9c5db199SXin Liimport collections 12*9c5db199SXin Liimport contextlib 13*9c5db199SXin Liimport json 14*9c5db199SXin Liimport logging 15*9c5db199SXin Liimport os 16*9c5db199SXin Liimport random 17*9c5db199SXin Liimport re 18*9c5db199SXin Liimport time 19*9c5db199SXin Liimport traceback 20*9c5db199SXin Lifrom six.moves import filter 21*9c5db199SXin Lifrom six.moves import range 22*9c5db199SXin Lifrom six.moves import urllib 23*9c5db199SXin Li 24*9c5db199SXin Liimport common 25*9c5db199SXin Lifrom autotest_lib.client.bin.result_tools import utils as result_utils 26*9c5db199SXin Lifrom autotest_lib.client.bin.result_tools import utils_lib as result_utils_lib 27*9c5db199SXin Lifrom autotest_lib.client.bin.result_tools import view as result_view 28*9c5db199SXin Lifrom autotest_lib.client.common_lib import lsbrelease_utils 29*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 30*9c5db199SXin Lifrom autotest_lib.client.common_lib import error 31*9c5db199SXin Lifrom autotest_lib.client.common_lib import file_utils 32*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 33*9c5db199SXin Lifrom autotest_lib.client.common_lib import host_queue_entry_states 34*9c5db199SXin Lifrom autotest_lib.client.common_lib import host_states 35*9c5db199SXin Lifrom autotest_lib.server.cros import provision 36*9c5db199SXin Lifrom autotest_lib.server.cros.dynamic_suite import constants 37*9c5db199SXin Lifrom autotest_lib.server.cros.dynamic_suite import job_status 38*9c5db199SXin Li 39*9c5db199SXin Litry: 40*9c5db199SXin Li from autotest_lib.utils.frozen_chromite.lib import metrics 41*9c5db199SXin Liexcept ImportError: 42*9c5db199SXin Li metrics = utils.metrics_mock 43*9c5db199SXin Li 44*9c5db199SXin Li 45*9c5db199SXin LiCONFIG = global_config.global_config 46*9c5db199SXin Li 47*9c5db199SXin LiLAB_GOOD_STATES = ('open', 'throttled') 48*9c5db199SXin Li 49*9c5db199SXin LiENABLE_DRONE_IN_RESTRICTED_SUBNET = CONFIG.get_config_value( 50*9c5db199SXin Li 'CROS', 'enable_drone_in_restricted_subnet', type=bool, 51*9c5db199SXin Li default=False) 52*9c5db199SXin Li 53*9c5db199SXin Li# Wait at most 10 mins for duts to go idle. 54*9c5db199SXin LiIDLE_DUT_WAIT_TIMEOUT = 600 55*9c5db199SXin Li 56*9c5db199SXin Li# Mapping between board name and build target. This is for special case handling 57*9c5db199SXin Li# for certain Android board that the board name and build target name does not 58*9c5db199SXin Li# match. 59*9c5db199SXin LiANDROID_TARGET_TO_BOARD_MAP = { 60*9c5db199SXin Li 'seed_l8150': 'gm4g_sprout', 61*9c5db199SXin Li 'bat_land': 'bat' 62*9c5db199SXin Li } 63*9c5db199SXin LiANDROID_BOARD_TO_TARGET_MAP = { 64*9c5db199SXin Li 'gm4g_sprout': 'seed_l8150', 65*9c5db199SXin Li 'bat': 'bat_land' 66*9c5db199SXin Li } 67*9c5db199SXin Li# Prefix for the metrics name for result size information. 68*9c5db199SXin LiRESULT_METRICS_PREFIX = 'chromeos/autotest/result_collection/' 69*9c5db199SXin Li 70*9c5db199SXin Liclass TestLabException(Exception): 71*9c5db199SXin Li """Exception raised when the Test Lab blocks a test or suite.""" 72*9c5db199SXin Li pass 73*9c5db199SXin Li 74*9c5db199SXin Li 75*9c5db199SXin Liclass ParseBuildNameException(Exception): 76*9c5db199SXin Li """Raised when ParseBuildName() cannot parse a build name.""" 77*9c5db199SXin Li pass 78*9c5db199SXin Li 79*9c5db199SXin Li 80*9c5db199SXin Liclass Singleton(type): 81*9c5db199SXin Li """Enforce that only one client class is instantiated per process.""" 82*9c5db199SXin Li _instances = {} 83*9c5db199SXin Li 84*9c5db199SXin Li def __call__(cls, *args, **kwargs): 85*9c5db199SXin Li """Fetch the instance of a class to use for subsequent calls.""" 86*9c5db199SXin Li if cls not in cls._instances: 87*9c5db199SXin Li cls._instances[cls] = super(Singleton, cls).__call__( 88*9c5db199SXin Li *args, **kwargs) 89*9c5db199SXin Li return cls._instances[cls] 90*9c5db199SXin Li 91*9c5db199SXin Liclass EmptyAFEHost(object): 92*9c5db199SXin Li """Object to represent an AFE host object when there is no AFE.""" 93*9c5db199SXin Li 94*9c5db199SXin Li def __init__(self): 95*9c5db199SXin Li """ 96*9c5db199SXin Li We'll be setting the instance attributes as we use them. Right now 97*9c5db199SXin Li we only use attributes and labels but as time goes by and other 98*9c5db199SXin Li attributes are used from an actual AFE Host object (check 99*9c5db199SXin Li rpc_interfaces.get_hosts()), we'll add them in here so users won't be 100*9c5db199SXin Li perplexed why their host's afe_host object complains that attribute 101*9c5db199SXin Li doesn't exist. 102*9c5db199SXin Li """ 103*9c5db199SXin Li self.attributes = {} 104*9c5db199SXin Li self.labels = [] 105*9c5db199SXin Li 106*9c5db199SXin Li 107*9c5db199SXin Lidef ParseBuildName(name): 108*9c5db199SXin Li """Format a build name, given board, type, milestone, and manifest num. 109*9c5db199SXin Li 110*9c5db199SXin Li @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a 111*9c5db199SXin Li relative build name, e.g. 'x86-alex-release/LATEST' 112*9c5db199SXin Li 113*9c5db199SXin Li @return board: board the manifest is for, e.g. x86-alex. 114*9c5db199SXin Li @return type: one of 'release', 'factory', or 'firmware' 115*9c5db199SXin Li @return milestone: (numeric) milestone the manifest was associated with. 116*9c5db199SXin Li Will be None for relative build names. 117*9c5db199SXin Li @return manifest: manifest number, e.g. '2015.0.0'. 118*9c5db199SXin Li Will be None for relative build names. 119*9c5db199SXin Li 120*9c5db199SXin Li """ 121*9c5db199SXin Li match = re.match(r'(trybot-)?(?P<board>[\w-]+?)(?:-chrome)?(?:-chromium)?' 122*9c5db199SXin Li r'-(?P<type>\w+)/(R(?P<milestone>\d+)-' 123*9c5db199SXin Li r'(?P<manifest>[\d.ab-]+)|LATEST)', 124*9c5db199SXin Li name) 125*9c5db199SXin Li if match and len(match.groups()) >= 5: 126*9c5db199SXin Li return (match.group('board'), match.group('type'), 127*9c5db199SXin Li match.group('milestone'), match.group('manifest')) 128*9c5db199SXin Li raise ParseBuildNameException('%s is a malformed build name.' % name) 129*9c5db199SXin Li 130*9c5db199SXin Li 131*9c5db199SXin Lidef get_labels_from_afe(hostname, label_prefix, afe): 132*9c5db199SXin Li """Retrieve a host's specific labels from the AFE. 133*9c5db199SXin Li 134*9c5db199SXin Li Looks for the host labels that have the form <label_prefix>:<value> 135*9c5db199SXin Li and returns the "<value>" part of the label. None is returned 136*9c5db199SXin Li if there is not a label matching the pattern 137*9c5db199SXin Li 138*9c5db199SXin Li @param hostname: hostname of given DUT. 139*9c5db199SXin Li @param label_prefix: prefix of label to be matched, e.g., |board:| 140*9c5db199SXin Li @param afe: afe instance. 141*9c5db199SXin Li 142*9c5db199SXin Li @returns A list of labels that match the prefix or 'None' 143*9c5db199SXin Li 144*9c5db199SXin Li """ 145*9c5db199SXin Li labels = afe.get_labels(name__startswith=label_prefix, 146*9c5db199SXin Li host__hostname__in=[hostname]) 147*9c5db199SXin Li if labels: 148*9c5db199SXin Li return [l.name.split(label_prefix, 1)[1] for l in labels] 149*9c5db199SXin Li 150*9c5db199SXin Li 151*9c5db199SXin Lidef get_label_from_afe(hostname, label_prefix, afe): 152*9c5db199SXin Li """Retrieve a host's specific label from the AFE. 153*9c5db199SXin Li 154*9c5db199SXin Li Looks for a host label that has the form <label_prefix>:<value> 155*9c5db199SXin Li and returns the "<value>" part of the label. None is returned 156*9c5db199SXin Li if there is not a label matching the pattern 157*9c5db199SXin Li 158*9c5db199SXin Li @param hostname: hostname of given DUT. 159*9c5db199SXin Li @param label_prefix: prefix of label to be matched, e.g., |board:| 160*9c5db199SXin Li @param afe: afe instance. 161*9c5db199SXin Li @returns the label that matches the prefix or 'None' 162*9c5db199SXin Li 163*9c5db199SXin Li """ 164*9c5db199SXin Li labels = get_labels_from_afe(hostname, label_prefix, afe) 165*9c5db199SXin Li if labels and len(labels) == 1: 166*9c5db199SXin Li return labels[0] 167*9c5db199SXin Li 168*9c5db199SXin Li 169*9c5db199SXin Lidef get_board_from_afe(hostname, afe): 170*9c5db199SXin Li """Retrieve given host's board from its labels in the AFE. 171*9c5db199SXin Li 172*9c5db199SXin Li Looks for a host label of the form "board:<board>", and 173*9c5db199SXin Li returns the "<board>" part of the label. `None` is returned 174*9c5db199SXin Li if there is not a single, unique label matching the pattern. 175*9c5db199SXin Li 176*9c5db199SXin Li @param hostname: hostname of given DUT. 177*9c5db199SXin Li @param afe: afe instance. 178*9c5db199SXin Li @returns board from label, or `None`. 179*9c5db199SXin Li 180*9c5db199SXin Li """ 181*9c5db199SXin Li return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe) 182*9c5db199SXin Li 183*9c5db199SXin Li 184*9c5db199SXin Lidef get_build_from_afe(hostname, afe): 185*9c5db199SXin Li """Retrieve the current build for given host from the AFE. 186*9c5db199SXin Li 187*9c5db199SXin Li Looks through the host's labels in the AFE to determine its build. 188*9c5db199SXin Li 189*9c5db199SXin Li @param hostname: hostname of given DUT. 190*9c5db199SXin Li @param afe: afe instance. 191*9c5db199SXin Li @returns The current build or None if it could not find it or if there 192*9c5db199SXin Li were multiple build labels assigned to this host. 193*9c5db199SXin Li 194*9c5db199SXin Li """ 195*9c5db199SXin Li prefix = provision.CROS_VERSION_PREFIX 196*9c5db199SXin Li build = get_label_from_afe(hostname, prefix + ':', afe) 197*9c5db199SXin Li if build: 198*9c5db199SXin Li return build 199*9c5db199SXin Li return None 200*9c5db199SXin Li 201*9c5db199SXin Li 202*9c5db199SXin Li# TODO(ayatane): Can be deleted 203*9c5db199SXin Lidef get_sheriffs(lab_only=False): 204*9c5db199SXin Li """ 205*9c5db199SXin Li Polls the javascript file that holds the identity of the sheriff and 206*9c5db199SXin Li parses it's output to return a list of chromium sheriff email addresses. 207*9c5db199SXin Li The javascript file can contain the ldap of more than one sheriff, eg: 208*9c5db199SXin Li document.write('sheriff_one, sheriff_two'). 209*9c5db199SXin Li 210*9c5db199SXin Li @param lab_only: if True, only pulls lab sheriff. 211*9c5db199SXin Li @return: A list of chroium.org sheriff email addresses to cc on the bug. 212*9c5db199SXin Li An empty list if failed to parse the javascript. 213*9c5db199SXin Li """ 214*9c5db199SXin Li return [] 215*9c5db199SXin Li 216*9c5db199SXin Li 217*9c5db199SXin Lidef remote_wget(source_url, dest_path, ssh_cmd): 218*9c5db199SXin Li """wget source_url from localhost to dest_path on remote host using ssh. 219*9c5db199SXin Li 220*9c5db199SXin Li @param source_url: The complete url of the source of the package to send. 221*9c5db199SXin Li @param dest_path: The path on the remote host's file system where we would 222*9c5db199SXin Li like to store the package. 223*9c5db199SXin Li @param ssh_cmd: The ssh command to use in performing the remote wget. 224*9c5db199SXin Li """ 225*9c5db199SXin Li wget_cmd = ("wget -O - %s | %s 'cat >%s'" % 226*9c5db199SXin Li (source_url, ssh_cmd, dest_path)) 227*9c5db199SXin Li utils.run(wget_cmd) 228*9c5db199SXin Li 229*9c5db199SXin Li 230*9c5db199SXin Li_MAX_LAB_STATUS_ATTEMPTS = 5 231*9c5db199SXin Lidef _get_lab_status(status_url): 232*9c5db199SXin Li """Grabs the current lab status and message. 233*9c5db199SXin Li 234*9c5db199SXin Li @returns The JSON object obtained from the given URL. 235*9c5db199SXin Li 236*9c5db199SXin Li """ 237*9c5db199SXin Li retry_waittime = 1 238*9c5db199SXin Li for _ in range(_MAX_LAB_STATUS_ATTEMPTS): 239*9c5db199SXin Li try: 240*9c5db199SXin Li response = urllib.request.urlopen(status_url) 241*9c5db199SXin Li except IOError as e: 242*9c5db199SXin Li logging.debug('Error occurred when grabbing the lab status: %s.', 243*9c5db199SXin Li e) 244*9c5db199SXin Li time.sleep(retry_waittime) 245*9c5db199SXin Li continue 246*9c5db199SXin Li # Check for successful response code. 247*9c5db199SXin Li if response.getcode() == 200: 248*9c5db199SXin Li return json.load(response) 249*9c5db199SXin Li time.sleep(retry_waittime) 250*9c5db199SXin Li return None 251*9c5db199SXin Li 252*9c5db199SXin Li 253*9c5db199SXin Lidef _decode_lab_status(lab_status, build): 254*9c5db199SXin Li """Decode lab status, and report exceptions as needed. 255*9c5db199SXin Li 256*9c5db199SXin Li Take a deserialized JSON object from the lab status page, and 257*9c5db199SXin Li interpret it to determine the actual lab status. Raise 258*9c5db199SXin Li exceptions as required to report when the lab is down. 259*9c5db199SXin Li 260*9c5db199SXin Li @param build: build name that we want to check the status of. 261*9c5db199SXin Li 262*9c5db199SXin Li @raises TestLabException Raised if a request to test for the given 263*9c5db199SXin Li status and build should be blocked. 264*9c5db199SXin Li """ 265*9c5db199SXin Li # First check if the lab is up. 266*9c5db199SXin Li if not lab_status['general_state'] in LAB_GOOD_STATES: 267*9c5db199SXin Li raise TestLabException('Chromium OS Test Lab is closed: ' 268*9c5db199SXin Li '%s.' % lab_status['message']) 269*9c5db199SXin Li 270*9c5db199SXin Li # Check if the build we wish to use is disabled. 271*9c5db199SXin Li # Lab messages should be in the format of: 272*9c5db199SXin Li # Lab is 'status' [regex ...] (comment) 273*9c5db199SXin Li # If the build name matches any regex, it will be blocked. 274*9c5db199SXin Li build_exceptions = re.search('\[(.*)\]', lab_status['message']) 275*9c5db199SXin Li if not build_exceptions or not build: 276*9c5db199SXin Li return 277*9c5db199SXin Li for build_pattern in build_exceptions.group(1).split(): 278*9c5db199SXin Li if re.match(build_pattern, build): 279*9c5db199SXin Li raise TestLabException('Chromium OS Test Lab is closed: ' 280*9c5db199SXin Li '%s matches %s.' % ( 281*9c5db199SXin Li build, build_pattern)) 282*9c5db199SXin Li return 283*9c5db199SXin Li 284*9c5db199SXin Li 285*9c5db199SXin Lidef is_in_lab(): 286*9c5db199SXin Li """Check if current Autotest instance is in lab 287*9c5db199SXin Li 288*9c5db199SXin Li @return: True if the Autotest instance is in lab. 289*9c5db199SXin Li """ 290*9c5db199SXin Li test_server_name = CONFIG.get_config_value('SERVER', 'hostname') 291*9c5db199SXin Li return test_server_name.startswith('cautotest') 292*9c5db199SXin Li 293*9c5db199SXin Li 294*9c5db199SXin Lidef check_lab_status(build): 295*9c5db199SXin Li """Check if the lab status allows us to schedule for a build. 296*9c5db199SXin Li 297*9c5db199SXin Li Checks if the lab is down, or if testing for the requested build 298*9c5db199SXin Li should be blocked. 299*9c5db199SXin Li 300*9c5db199SXin Li @param build: Name of the build to be scheduled for testing. 301*9c5db199SXin Li 302*9c5db199SXin Li @raises TestLabException Raised if a request to test for the given 303*9c5db199SXin Li status and build should be blocked. 304*9c5db199SXin Li 305*9c5db199SXin Li """ 306*9c5db199SXin Li # Ensure we are trying to schedule on the actual lab. 307*9c5db199SXin Li if not is_in_lab(): 308*9c5db199SXin Li return 309*9c5db199SXin Li 310*9c5db199SXin Li # Download the lab status from its home on the web. 311*9c5db199SXin Li status_url = CONFIG.get_config_value('CROS', 'lab_status_url') 312*9c5db199SXin Li json_status = _get_lab_status(status_url) 313*9c5db199SXin Li if json_status is None: 314*9c5db199SXin Li # We go ahead and say the lab is open if we can't get the status. 315*9c5db199SXin Li logging.warning('Could not get a status from %s', status_url) 316*9c5db199SXin Li return 317*9c5db199SXin Li _decode_lab_status(json_status, build) 318*9c5db199SXin Li 319*9c5db199SXin Li 320*9c5db199SXin Lidef host_in_lab(hostname): 321*9c5db199SXin Li """Check if the execution is against a host in the lab""" 322*9c5db199SXin Li return (not utils.in_moblab_ssp() 323*9c5db199SXin Li and not lsbrelease_utils.is_moblab() 324*9c5db199SXin Li and utils.host_is_in_lab_zone(hostname)) 325*9c5db199SXin Li 326*9c5db199SXin Li 327*9c5db199SXin Lidef lock_host_with_labels(afe, lock_manager, labels): 328*9c5db199SXin Li """Lookup and lock one host that matches the list of input labels. 329*9c5db199SXin Li 330*9c5db199SXin Li @param afe: An instance of the afe class, as defined in server.frontend. 331*9c5db199SXin Li @param lock_manager: A lock manager capable of locking hosts, eg the 332*9c5db199SXin Li one defined in server.cros.host_lock_manager. 333*9c5db199SXin Li @param labels: A list of labels to look for on hosts. 334*9c5db199SXin Li 335*9c5db199SXin Li @return: The hostname of a host matching all labels, and locked through the 336*9c5db199SXin Li lock_manager. The hostname will be as specified in the database the afe 337*9c5db199SXin Li object is associated with, i.e if it exists in afe_hosts with a .cros 338*9c5db199SXin Li suffix, the hostname returned will contain a .cros suffix. 339*9c5db199SXin Li 340*9c5db199SXin Li @raises: error.NoEligibleHostException: If no hosts matching the list of 341*9c5db199SXin Li input labels are available. 342*9c5db199SXin Li @raises: error.TestError: If unable to lock a host matching the labels. 343*9c5db199SXin Li """ 344*9c5db199SXin Li potential_hosts = afe.get_hosts(multiple_labels=labels) 345*9c5db199SXin Li if not potential_hosts: 346*9c5db199SXin Li raise error.NoEligibleHostException( 347*9c5db199SXin Li 'No devices found with labels %s.' % labels) 348*9c5db199SXin Li 349*9c5db199SXin Li # This prevents errors where a fault might seem repeatable 350*9c5db199SXin Li # because we lock, say, the same packet capturer for each test run. 351*9c5db199SXin Li random.shuffle(potential_hosts) 352*9c5db199SXin Li for host in potential_hosts: 353*9c5db199SXin Li if lock_manager.lock([host.hostname]): 354*9c5db199SXin Li logging.info('Locked device %s with labels %s.', 355*9c5db199SXin Li host.hostname, labels) 356*9c5db199SXin Li return host.hostname 357*9c5db199SXin Li else: 358*9c5db199SXin Li logging.info('Unable to lock device %s with labels %s.', 359*9c5db199SXin Li host.hostname, labels) 360*9c5db199SXin Li 361*9c5db199SXin Li raise error.TestError('Could not lock a device with labels %s' % labels) 362*9c5db199SXin Li 363*9c5db199SXin Li 364*9c5db199SXin Lidef get_test_views_from_tko(suite_job_id, tko): 365*9c5db199SXin Li """Get test name and result for given suite job ID. 366*9c5db199SXin Li 367*9c5db199SXin Li @param suite_job_id: ID of suite job. 368*9c5db199SXin Li @param tko: an instance of TKO as defined in server/frontend.py. 369*9c5db199SXin Li @return: A defaultdict where keys are test names and values are 370*9c5db199SXin Li lists of test statuses, e.g., 371*9c5db199SXin Li {'stub_Fail.Error': ['ERROR'. 'ERROR'], 372*9c5db199SXin Li 'stub_Fail.NAError': ['TEST_NA'], 373*9c5db199SXin Li 'stub_Fail.RetrySuccess': ['ERROR', 'GOOD'], 374*9c5db199SXin Li } 375*9c5db199SXin Li @raise: Exception when there is no test view found. 376*9c5db199SXin Li 377*9c5db199SXin Li """ 378*9c5db199SXin Li views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id) 379*9c5db199SXin Li relevant_views = list(filter(job_status.view_is_relevant, views)) 380*9c5db199SXin Li if not relevant_views: 381*9c5db199SXin Li raise Exception('Failed to retrieve job results.') 382*9c5db199SXin Li 383*9c5db199SXin Li test_views = collections.defaultdict(list) 384*9c5db199SXin Li for view in relevant_views: 385*9c5db199SXin Li test_views[view['test_name']].append(view['status']) 386*9c5db199SXin Li return test_views 387*9c5db199SXin Li 388*9c5db199SXin Li 389*9c5db199SXin Lidef get_data_key(prefix, suite, build, board): 390*9c5db199SXin Li """ 391*9c5db199SXin Li Constructs a key string from parameters. 392*9c5db199SXin Li 393*9c5db199SXin Li @param prefix: Prefix for the generating key. 394*9c5db199SXin Li @param suite: a suite name. e.g., bvt-cq, bvt-inline, infra_qual 395*9c5db199SXin Li @param build: The build string. This string should have a consistent 396*9c5db199SXin Li format eg: x86-mario-release/R26-3570.0.0. If the format of this 397*9c5db199SXin Li string changes such that we can't determine build_type or branch 398*9c5db199SXin Li we give up and use the parametes we're sure of instead (suite, 399*9c5db199SXin Li board). eg: 400*9c5db199SXin Li 1. build = x86-alex-pgo-release/R26-3570.0.0 401*9c5db199SXin Li branch = 26 402*9c5db199SXin Li build_type = pgo-release 403*9c5db199SXin Li 2. build = lumpy-paladin/R28-3993.0.0-rc5 404*9c5db199SXin Li branch = 28 405*9c5db199SXin Li build_type = paladin 406*9c5db199SXin Li @param board: The board that this suite ran on. 407*9c5db199SXin Li @return: The key string used for a dictionary. 408*9c5db199SXin Li """ 409*9c5db199SXin Li try: 410*9c5db199SXin Li _board, build_type, branch = ParseBuildName(build)[:3] 411*9c5db199SXin Li except ParseBuildNameException as e: 412*9c5db199SXin Li logging.error(str(e)) 413*9c5db199SXin Li branch = 'Unknown' 414*9c5db199SXin Li build_type = 'Unknown' 415*9c5db199SXin Li else: 416*9c5db199SXin Li embedded_str = re.search(r'x86-\w+-(.*)', _board) 417*9c5db199SXin Li if embedded_str: 418*9c5db199SXin Li build_type = embedded_str.group(1) + '-' + build_type 419*9c5db199SXin Li 420*9c5db199SXin Li data_key_dict = { 421*9c5db199SXin Li 'prefix': prefix, 422*9c5db199SXin Li 'board': board, 423*9c5db199SXin Li 'branch': branch, 424*9c5db199SXin Li 'build_type': build_type, 425*9c5db199SXin Li 'suite': suite, 426*9c5db199SXin Li } 427*9c5db199SXin Li return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s' 428*9c5db199SXin Li % data_key_dict) 429*9c5db199SXin Li 430*9c5db199SXin Li 431*9c5db199SXin Lidef is_shard(): 432*9c5db199SXin Li """Determines if this instance is running as a shard. 433*9c5db199SXin Li 434*9c5db199SXin Li Reads the global_config value shard_hostname in the section SHARD. 435*9c5db199SXin Li 436*9c5db199SXin Li @return True, if shard_hostname is set, False otherwise. 437*9c5db199SXin Li """ 438*9c5db199SXin Li hostname = CONFIG.get_config_value('SHARD', 'shard_hostname', default=None) 439*9c5db199SXin Li return bool(hostname) 440*9c5db199SXin Li 441*9c5db199SXin Li 442*9c5db199SXin Lidef get_global_afe_hostname(): 443*9c5db199SXin Li """Read the hostname of the global AFE from the global configuration.""" 444*9c5db199SXin Li return CONFIG.get_config_value('SERVER', 'global_afe_hostname') 445*9c5db199SXin Li 446*9c5db199SXin Li 447*9c5db199SXin Lidef get_special_task_status(is_complete, success, is_active): 448*9c5db199SXin Li """Get the status of a special task. 449*9c5db199SXin Li 450*9c5db199SXin Li Emulate a host queue entry status for a special task 451*9c5db199SXin Li Although SpecialTasks are not HostQueueEntries, it is helpful to 452*9c5db199SXin Li the user to present similar statuses. 453*9c5db199SXin Li 454*9c5db199SXin Li @param is_complete Boolean if the task is completed. 455*9c5db199SXin Li @param success Boolean if the task succeeded. 456*9c5db199SXin Li @param is_active Boolean if the task is active. 457*9c5db199SXin Li 458*9c5db199SXin Li @return The status of a special task. 459*9c5db199SXin Li """ 460*9c5db199SXin Li if is_complete: 461*9c5db199SXin Li if success: 462*9c5db199SXin Li return host_queue_entry_states.Status.COMPLETED 463*9c5db199SXin Li return host_queue_entry_states.Status.FAILED 464*9c5db199SXin Li if is_active: 465*9c5db199SXin Li return host_queue_entry_states.Status.RUNNING 466*9c5db199SXin Li return host_queue_entry_states.Status.QUEUED 467*9c5db199SXin Li 468*9c5db199SXin Li 469*9c5db199SXin Lidef get_special_task_exec_path(hostname, task_id, task_name, time_requested): 470*9c5db199SXin Li """Get the execution path of the SpecialTask. 471*9c5db199SXin Li 472*9c5db199SXin Li This method returns different paths depending on where a 473*9c5db199SXin Li the task ran: 474*9c5db199SXin Li * main: hosts/hostname/task_id-task_type 475*9c5db199SXin Li * Shard: main_path/time_created 476*9c5db199SXin Li This is to work around the fact that a shard can fail independent 477*9c5db199SXin Li of the main, and be replaced by another shard that has the same 478*9c5db199SXin Li hosts. Without the time_created stamp the logs of the tasks running 479*9c5db199SXin Li on the second shard will clobber the logs from the first in google 480*9c5db199SXin Li storage, because task ids are not globally unique. 481*9c5db199SXin Li 482*9c5db199SXin Li @param hostname Hostname 483*9c5db199SXin Li @param task_id Special task id 484*9c5db199SXin Li @param task_name Special task name (e.g., Verify, Repair, etc) 485*9c5db199SXin Li @param time_requested Special task requested time. 486*9c5db199SXin Li 487*9c5db199SXin Li @return An execution path for the task. 488*9c5db199SXin Li """ 489*9c5db199SXin Li results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower()) 490*9c5db199SXin Li 491*9c5db199SXin Li # If we do this on the main it will break backward compatibility, 492*9c5db199SXin Li # as there are tasks that currently don't have timestamps. If a host 493*9c5db199SXin Li # or job has been sent to a shard, the rpc for that host/job will 494*9c5db199SXin Li # be redirected to the shard, so this global_config check will happen 495*9c5db199SXin Li # on the shard the logs are on. 496*9c5db199SXin Li if not is_shard(): 497*9c5db199SXin Li return results_path 498*9c5db199SXin Li 499*9c5db199SXin Li # Generate a uid to disambiguate special task result directories 500*9c5db199SXin Li # in case this shard fails. The simplest uid is the job_id, however 501*9c5db199SXin Li # in rare cases tasks do not have jobs associated with them (eg: 502*9c5db199SXin Li # frontend verify), so just use the creation timestamp. The clocks 503*9c5db199SXin Li # between a shard and main should always be in sync. Any discrepancies 504*9c5db199SXin Li # will be brought to our attention in the form of job timeouts. 505*9c5db199SXin Li uid = time_requested.strftime('%Y%d%m%H%M%S') 506*9c5db199SXin Li 507*9c5db199SXin Li # TODO: This is a hack, however it is the easiest way to achieve 508*9c5db199SXin Li # correctness. There is currently some debate over the future of 509*9c5db199SXin Li # tasks in our infrastructure and refactoring everything right 510*9c5db199SXin Li # now isn't worth the time. 511*9c5db199SXin Li return '%s/%s' % (results_path, uid) 512*9c5db199SXin Li 513*9c5db199SXin Li 514*9c5db199SXin Lidef get_job_tag(id, owner): 515*9c5db199SXin Li """Returns a string tag for a job. 516*9c5db199SXin Li 517*9c5db199SXin Li @param id Job id 518*9c5db199SXin Li @param owner Job owner 519*9c5db199SXin Li 520*9c5db199SXin Li """ 521*9c5db199SXin Li return '%s-%s' % (id, owner) 522*9c5db199SXin Li 523*9c5db199SXin Li 524*9c5db199SXin Lidef get_hqe_exec_path(tag, execution_subdir): 525*9c5db199SXin Li """Returns a execution path to a HQE's results. 526*9c5db199SXin Li 527*9c5db199SXin Li @param tag Tag string for a job associated with a HQE. 528*9c5db199SXin Li @param execution_subdir Execution sub-directory string of a HQE. 529*9c5db199SXin Li 530*9c5db199SXin Li """ 531*9c5db199SXin Li return os.path.join(tag, execution_subdir) 532*9c5db199SXin Li 533*9c5db199SXin Li 534*9c5db199SXin Lidef is_inside_chroot(): 535*9c5db199SXin Li """Check if the process is running inside chroot. 536*9c5db199SXin Li 537*9c5db199SXin Li @return: True if the process is running inside chroot. 538*9c5db199SXin Li 539*9c5db199SXin Li """ 540*9c5db199SXin Li return os.path.exists('/etc/cros_chroot_version') 541*9c5db199SXin Li 542*9c5db199SXin Li 543*9c5db199SXin Lidef parse_job_name(name): 544*9c5db199SXin Li """Parse job name to get information including build, board and suite etc. 545*9c5db199SXin Li 546*9c5db199SXin Li Suite job created by run_suite follows the naming convention of: 547*9c5db199SXin Li [build]-test_suites/control.[suite] 548*9c5db199SXin Li For example: lumpy-release/R46-7272.0.0-test_suites/control.bvt 549*9c5db199SXin Li The naming convention is defined in rpc_interface.create_suite_job. 550*9c5db199SXin Li 551*9c5db199SXin Li Test job created by suite job follows the naming convention of: 552*9c5db199SXin Li [build]/[suite]/[test name] 553*9c5db199SXin Li For example: lumpy-release/R46-7272.0.0/bvt/login_LoginSuccess 554*9c5db199SXin Li The naming convention is defined in 555*9c5db199SXin Li server/cros/dynamic_suite/tools.create_job_name 556*9c5db199SXin Li 557*9c5db199SXin Li Note that pgo and chrome-perf builds will fail the method. Since lab does 558*9c5db199SXin Li not run test for these builds, they can be ignored. 559*9c5db199SXin Li Also, tests for Launch Control builds have different naming convention. 560*9c5db199SXin Li The build ID will be used as build_version. 561*9c5db199SXin Li 562*9c5db199SXin Li @param name: Name of the job. 563*9c5db199SXin Li 564*9c5db199SXin Li @return: A dictionary containing the test information. The keyvals include: 565*9c5db199SXin Li build: Name of the build, e.g., lumpy-release/R46-7272.0.0 566*9c5db199SXin Li build_version: The version of the build, e.g., R46-7272.0.0 567*9c5db199SXin Li board: Name of the board, e.g., lumpy 568*9c5db199SXin Li suite: Name of the test suite, e.g., bvt 569*9c5db199SXin Li 570*9c5db199SXin Li """ 571*9c5db199SXin Li info = {} 572*9c5db199SXin Li suite_job_regex = '([^/]*/[^/]*(?:/\d+)?)-test_suites/control\.(.*)' 573*9c5db199SXin Li test_job_regex = '([^/]*/[^/]*(?:/\d+)?)/([^/]+)/.*' 574*9c5db199SXin Li match = re.match(suite_job_regex, name) 575*9c5db199SXin Li if not match: 576*9c5db199SXin Li match = re.match(test_job_regex, name) 577*9c5db199SXin Li if match: 578*9c5db199SXin Li info['build'] = match.groups()[0] 579*9c5db199SXin Li info['suite'] = match.groups()[1] 580*9c5db199SXin Li info['build_version'] = info['build'].split('/')[1] 581*9c5db199SXin Li try: 582*9c5db199SXin Li info['board'], _, _, _ = ParseBuildName(info['build']) 583*9c5db199SXin Li except ParseBuildNameException: 584*9c5db199SXin Li # Try to parse it as Launch Control build 585*9c5db199SXin Li # Launch Control builds have name format: 586*9c5db199SXin Li # branch/build_target-build_type/build_id. 587*9c5db199SXin Li try: 588*9c5db199SXin Li _, target, build_id = utils.parse_launch_control_build( 589*9c5db199SXin Li info['build']) 590*9c5db199SXin Li build_target, _ = utils.parse_launch_control_target(target) 591*9c5db199SXin Li if build_target: 592*9c5db199SXin Li info['board'] = build_target 593*9c5db199SXin Li info['build_version'] = build_id 594*9c5db199SXin Li except ValueError: 595*9c5db199SXin Li pass 596*9c5db199SXin Li return info 597*9c5db199SXin Li 598*9c5db199SXin Li 599*9c5db199SXin Lidef verify_not_root_user(): 600*9c5db199SXin Li """Simple function to error out if running with uid == 0""" 601*9c5db199SXin Li if os.getuid() == 0: 602*9c5db199SXin Li raise error.IllegalUser('This script can not be ran as root.') 603*9c5db199SXin Li 604*9c5db199SXin Li 605*9c5db199SXin Lidef get_hostname_from_machine(machine): 606*9c5db199SXin Li """Lookup hostname from a machine string or dict. 607*9c5db199SXin Li 608*9c5db199SXin Li @returns: Machine hostname in string format. 609*9c5db199SXin Li """ 610*9c5db199SXin Li hostname, _ = get_host_info_from_machine(machine) 611*9c5db199SXin Li return hostname 612*9c5db199SXin Li 613*9c5db199SXin Li 614*9c5db199SXin Lidef get_host_info_from_machine(machine): 615*9c5db199SXin Li """Lookup host information from a machine string or dict. 616*9c5db199SXin Li 617*9c5db199SXin Li @returns: Tuple of (hostname, afe_host) 618*9c5db199SXin Li """ 619*9c5db199SXin Li if isinstance(machine, dict): 620*9c5db199SXin Li return (machine['hostname'], machine['afe_host']) 621*9c5db199SXin Li else: 622*9c5db199SXin Li return (machine, EmptyAFEHost()) 623*9c5db199SXin Li 624*9c5db199SXin Li 625*9c5db199SXin Lidef get_afe_host_from_machine(machine): 626*9c5db199SXin Li """Return the afe_host from the machine dict if possible. 627*9c5db199SXin Li 628*9c5db199SXin Li @returns: AFE host object. 629*9c5db199SXin Li """ 630*9c5db199SXin Li _, afe_host = get_host_info_from_machine(machine) 631*9c5db199SXin Li return afe_host 632*9c5db199SXin Li 633*9c5db199SXin Li 634*9c5db199SXin Lidef get_connection_pool_from_machine(machine): 635*9c5db199SXin Li """Returns the ssh_multiplex.ConnectionPool from machine if possible.""" 636*9c5db199SXin Li if not isinstance(machine, dict): 637*9c5db199SXin Li return None 638*9c5db199SXin Li return machine.get('connection_pool') 639*9c5db199SXin Li 640*9c5db199SXin Li 641*9c5db199SXin Lidef SetupTsMonGlobalState(*args, **kwargs): 642*9c5db199SXin Li """Import-safe wrap around chromite.lib.ts_mon_config's setup function. 643*9c5db199SXin Li 644*9c5db199SXin Li @param *args: Args to pass through. 645*9c5db199SXin Li @param **kwargs: Kwargs to pass through. 646*9c5db199SXin Li """ 647*9c5db199SXin Li try: 648*9c5db199SXin Li # TODO(crbug.com/739466) This module import is delayed because it adds 649*9c5db199SXin Li # 1-2 seconds to the module import time and most users of site_utils 650*9c5db199SXin Li # don't need it. The correct fix is to break apart site_utils into more 651*9c5db199SXin Li # meaningful chunks. 652*9c5db199SXin Li from autotest_lib.utils.frozen_chromite.lib import ts_mon_config 653*9c5db199SXin Li except ImportError as e: 654*9c5db199SXin Li logging.warning('Unable to import chromite. Monarch is disabled: %s', e) 655*9c5db199SXin Li return TrivialContextManager() 656*9c5db199SXin Li 657*9c5db199SXin Li try: 658*9c5db199SXin Li context = ts_mon_config.SetupTsMonGlobalState(*args, **kwargs) 659*9c5db199SXin Li if hasattr(context, '__exit__'): 660*9c5db199SXin Li return context 661*9c5db199SXin Li except Exception as e: 662*9c5db199SXin Li logging.warning('Caught an exception trying to setup ts_mon, ' 663*9c5db199SXin Li 'monitoring is disabled: %s', e, exc_info=True) 664*9c5db199SXin Li return TrivialContextManager() 665*9c5db199SXin Li 666*9c5db199SXin Li 667*9c5db199SXin Li@contextlib.contextmanager 668*9c5db199SXin Lidef TrivialContextManager(*args, **kwargs): 669*9c5db199SXin Li """Context manager that does nothing. 670*9c5db199SXin Li 671*9c5db199SXin Li @param *args: Ignored args 672*9c5db199SXin Li @param **kwargs: Ignored kwargs. 673*9c5db199SXin Li """ 674*9c5db199SXin Li yield 675*9c5db199SXin Li 676*9c5db199SXin Li 677*9c5db199SXin Lidef wait_for_idle_duts(duts, afe, max_wait=IDLE_DUT_WAIT_TIMEOUT): 678*9c5db199SXin Li """Wait for the hosts to all go idle. 679*9c5db199SXin Li 680*9c5db199SXin Li @param duts: List of duts to check for idle state. 681*9c5db199SXin Li @param afe: afe instance. 682*9c5db199SXin Li @param max_wait: Max wait time in seconds to wait for duts to be idle. 683*9c5db199SXin Li 684*9c5db199SXin Li @returns Boolean True if all hosts are idle or False if any hosts did not 685*9c5db199SXin Li go idle within max_wait. 686*9c5db199SXin Li """ 687*9c5db199SXin Li start_time = time.time() 688*9c5db199SXin Li # We make a shallow copy since we're going to be modifying active_dut_list. 689*9c5db199SXin Li active_dut_list = duts[:] 690*9c5db199SXin Li while active_dut_list: 691*9c5db199SXin Li # Let's rate-limit how often we hit the AFE. 692*9c5db199SXin Li time.sleep(1) 693*9c5db199SXin Li 694*9c5db199SXin Li # Check if we've waited too long. 695*9c5db199SXin Li if (time.time() - start_time) > max_wait: 696*9c5db199SXin Li return False 697*9c5db199SXin Li 698*9c5db199SXin Li idle_duts = [] 699*9c5db199SXin Li # Get the status for the duts and see if they're in the idle state. 700*9c5db199SXin Li afe_hosts = afe.get_hosts(active_dut_list) 701*9c5db199SXin Li idle_duts = [afe_host.hostname for afe_host in afe_hosts 702*9c5db199SXin Li if afe_host.status in host_states.IDLE_STATES] 703*9c5db199SXin Li 704*9c5db199SXin Li # Take out idle duts so we don't needlessly check them 705*9c5db199SXin Li # next time around. 706*9c5db199SXin Li for idle_dut in idle_duts: 707*9c5db199SXin Li active_dut_list.remove(idle_dut) 708*9c5db199SXin Li 709*9c5db199SXin Li logging.info('still waiting for following duts to go idle: %s', 710*9c5db199SXin Li active_dut_list) 711*9c5db199SXin Li return True 712*9c5db199SXin Li 713*9c5db199SXin Li 714*9c5db199SXin Li@contextlib.contextmanager 715*9c5db199SXin Lidef lock_duts_and_wait(duts, afe, lock_msg='default lock message', 716*9c5db199SXin Li max_wait=IDLE_DUT_WAIT_TIMEOUT): 717*9c5db199SXin Li """Context manager to lock the duts and wait for them to go idle. 718*9c5db199SXin Li 719*9c5db199SXin Li @param duts: List of duts to lock. 720*9c5db199SXin Li @param afe: afe instance. 721*9c5db199SXin Li @param lock_msg: message for afe on locking this host. 722*9c5db199SXin Li @param max_wait: Max wait time in seconds to wait for duts to be idle. 723*9c5db199SXin Li 724*9c5db199SXin Li @returns Boolean lock_success where True if all duts locked successfully or 725*9c5db199SXin Li False if we timed out waiting too long for hosts to go idle. 726*9c5db199SXin Li """ 727*9c5db199SXin Li try: 728*9c5db199SXin Li locked_duts = [] 729*9c5db199SXin Li duts.sort() 730*9c5db199SXin Li for dut in duts: 731*9c5db199SXin Li if afe.lock_host(dut, lock_msg, fail_if_locked=True): 732*9c5db199SXin Li locked_duts.append(dut) 733*9c5db199SXin Li else: 734*9c5db199SXin Li logging.info('%s already locked', dut) 735*9c5db199SXin Li yield wait_for_idle_duts(locked_duts, afe, max_wait) 736*9c5db199SXin Li finally: 737*9c5db199SXin Li afe.unlock_hosts(locked_duts) 738*9c5db199SXin Li 739*9c5db199SXin Li 740*9c5db199SXin Lidef _get_default_size_info(path): 741*9c5db199SXin Li """Get the default result size information. 742*9c5db199SXin Li 743*9c5db199SXin Li In case directory summary is failed to build, assume the test result is not 744*9c5db199SXin Li throttled and all result sizes are the size of existing test results. 745*9c5db199SXin Li 746*9c5db199SXin Li @return: A namedtuple of result size informations, including: 747*9c5db199SXin Li client_result_collected_KB: The total size (in KB) of test results 748*9c5db199SXin Li collected from test device. Set to be the total size of the 749*9c5db199SXin Li given path. 750*9c5db199SXin Li original_result_total_KB: The original size (in KB) of test results 751*9c5db199SXin Li before being trimmed. Set to be the total size of the given 752*9c5db199SXin Li path. 753*9c5db199SXin Li result_uploaded_KB: The total size (in KB) of test results to be 754*9c5db199SXin Li uploaded. Set to be the total size of the given path. 755*9c5db199SXin Li result_throttled: True if test results collection is throttled. 756*9c5db199SXin Li It's set to False in this default behavior. 757*9c5db199SXin Li """ 758*9c5db199SXin Li total_size = file_utils.get_directory_size_kibibytes(path); 759*9c5db199SXin Li return result_utils_lib.ResultSizeInfo( 760*9c5db199SXin Li client_result_collected_KB=total_size, 761*9c5db199SXin Li original_result_total_KB=total_size, 762*9c5db199SXin Li result_uploaded_KB=total_size, 763*9c5db199SXin Li result_throttled=False) 764*9c5db199SXin Li 765*9c5db199SXin Li 766*9c5db199SXin Lidef _report_result_size_metrics(result_size_info): 767*9c5db199SXin Li """Report result sizes information to metrics. 768*9c5db199SXin Li 769*9c5db199SXin Li @param result_size_info: A ResultSizeInfo namedtuple containing information 770*9c5db199SXin Li of test result sizes. 771*9c5db199SXin Li """ 772*9c5db199SXin Li fields = {'result_throttled' : result_size_info.result_throttled} 773*9c5db199SXin Li metrics.Counter(RESULT_METRICS_PREFIX + 'client_result_collected_KB', 774*9c5db199SXin Li description='The total size (in KB) of test results ' 775*9c5db199SXin Li 'collected from test device. Set to be the total size of ' 776*9c5db199SXin Li 'the given path.').increment_by(int( 777*9c5db199SXin Li result_size_info.client_result_collected_KB), 778*9c5db199SXin Li fields=fields) 779*9c5db199SXin Li metrics.Counter(RESULT_METRICS_PREFIX + 'original_result_total_KB', 780*9c5db199SXin Li description='The original size (in KB) of test results ' 781*9c5db199SXin Li 'before being trimmed.').increment_by(int( 782*9c5db199SXin Li result_size_info.original_result_total_KB), 783*9c5db199SXin Li fields=fields) 784*9c5db199SXin Li metrics.Counter(RESULT_METRICS_PREFIX + 'result_uploaded_KB', 785*9c5db199SXin Li description='The total size (in KB) of test results to be ' 786*9c5db199SXin Li 'uploaded.').increment_by(int( 787*9c5db199SXin Li result_size_info.result_uploaded_KB), 788*9c5db199SXin Li fields=fields) 789*9c5db199SXin Li 790*9c5db199SXin Li 791*9c5db199SXin Li@metrics.SecondsTimerDecorator( 792*9c5db199SXin Li 'chromeos/autotest/result_collection/collect_result_sizes_duration') 793*9c5db199SXin Lidef collect_result_sizes(path, log=logging.debug): 794*9c5db199SXin Li """Collect the result sizes information and build result summary. 795*9c5db199SXin Li 796*9c5db199SXin Li It first tries to merge directory summaries and calculate the result sizes 797*9c5db199SXin Li including: 798*9c5db199SXin Li client_result_collected_KB: The volume in KB that's transfered from the test 799*9c5db199SXin Li device. 800*9c5db199SXin Li original_result_total_KB: The volume in KB that's the original size of the 801*9c5db199SXin Li result files before being trimmed. 802*9c5db199SXin Li result_uploaded_KB: The volume in KB that will be uploaded. 803*9c5db199SXin Li result_throttled: Indicating if the result files were throttled. 804*9c5db199SXin Li 805*9c5db199SXin Li If directory summary merging failed for any reason, fall back to use the 806*9c5db199SXin Li total size of the given result directory. 807*9c5db199SXin Li 808*9c5db199SXin Li @param path: Path of the result directory to get size information. 809*9c5db199SXin Li @param log: The logging method, default to logging.debug 810*9c5db199SXin Li @return: A ResultSizeInfo namedtuple containing information of test result 811*9c5db199SXin Li sizes. 812*9c5db199SXin Li """ 813*9c5db199SXin Li try: 814*9c5db199SXin Li client_collected_bytes, summary, files = result_utils.merge_summaries( 815*9c5db199SXin Li path) 816*9c5db199SXin Li result_size_info = result_utils_lib.get_result_size_info( 817*9c5db199SXin Li client_collected_bytes, summary) 818*9c5db199SXin Li html_file = os.path.join(path, result_view.DEFAULT_RESULT_SUMMARY_NAME) 819*9c5db199SXin Li result_view.build(client_collected_bytes, summary, html_file) 820*9c5db199SXin Li 821*9c5db199SXin Li # Delete all summary files after final view is built. 822*9c5db199SXin Li for summary_file in files: 823*9c5db199SXin Li os.remove(summary_file) 824*9c5db199SXin Li except: 825*9c5db199SXin Li log('Failed to calculate result sizes based on directory summaries for ' 826*9c5db199SXin Li 'directory %s. Fall back to record the total size.\nException: %s' % 827*9c5db199SXin Li (path, traceback.format_exc())) 828*9c5db199SXin Li result_size_info = _get_default_size_info(path) 829*9c5db199SXin Li 830*9c5db199SXin Li _report_result_size_metrics(result_size_info) 831*9c5db199SXin Li 832*9c5db199SXin Li return result_size_info 833