xref: /aosp_15_r20/external/autotest/server/site_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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