Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import json
      6 import logging
      7 import os
      8 import StringIO
      9 
     10 from autotest_lib.client.common_lib import error, utils
     11 from autotest_lib.client.common_lib.cros import dev_server
     12 
     13 
     14 TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_benchmark'
     15 TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
     16 TELEMETRY_RUN_GPU_TESTS_SCRIPT = 'content/test/gpu/run_gpu_integration_test.py'
     17 TELEMETRY_TIMEOUT_MINS = 150
     18 
     19 DUT_CHROME_ROOT = '/usr/local/telemetry/src'
     20 
     21 # Result Statuses
     22 SUCCESS_STATUS = 'SUCCESS'
     23 WARNING_STATUS = 'WARNING'
     24 FAILED_STATUS = 'FAILED'
     25 
     26 # A list of telemetry tests that cannot run on dut.
     27 ON_DUT_BLACKLIST = [
     28     'loading.desktop',              # crbug/882299
     29     'rendering.desktop',            # crbug/882291
     30     'system_health.memory_desktop', # crbug/874386
     31 ]
     32 
     33 class TelemetryResult(object):
     34     """Class to represent the results of a telemetry run.
     35 
     36     This class represents the results of a telemetry run, whether it ran
     37     successful, failed or had warnings.
     38     """
     39 
     40 
     41     def __init__(self, exit_code=0, stdout='', stderr=''):
     42         """Initializes this TelemetryResultObject instance.
     43 
     44         @param status: Status of the telemtry run.
     45         @param stdout: Stdout of the telemetry run.
     46         @param stderr: Stderr of the telemetry run.
     47         """
     48         if exit_code == 0:
     49             self.status = SUCCESS_STATUS
     50         else:
     51             self.status = FAILED_STATUS
     52 
     53         self._stdout = stdout
     54         self._stderr = stderr
     55         self.output = '\n'.join([stdout, stderr])
     56 
     57 
     58 class TelemetryRunner(object):
     59     """Class responsible for telemetry for a given build.
     60 
     61     This class will extract and install telemetry on the devserver and is
     62     responsible for executing the telemetry benchmarks and returning their
     63     output to the caller.
     64     """
     65 
     66     def __init__(self, host, local=False, telemetry_on_dut=True):
     67         """Initializes this telemetry runner instance.
     68 
     69         If telemetry is not installed for this build, it will be.
     70 
     71         Basically, the following commands on the local pc on which test_that
     72         will be executed, depending on the 4 possible combinations of
     73         local x telemetry_on_dut:
     74 
     75         local=True, telemetry_on_dut=False:
     76         run_benchmark --browser=cros-chrome --remote=[dut] [test]
     77 
     78         local=True, telemetry_on_dut=True:
     79         ssh [dut] run_benchmark --browser=system [test]
     80 
     81         local=False, telemetry_on_dut=False:
     82         ssh [devserver] run_benchmark --browser=cros-chrome --remote=[dut] [test]
     83 
     84         local=False, telemetry_on_dut=True:
     85         ssh [devserver] ssh [dut] run_benchmark --browser=system [test]
     86 
     87         @param host: Host where the test will be run.
     88         @param local: If set, no devserver will be used, test will be run
     89                       locally.
     90                       If not set, "ssh [devserver] " will be appended to test
     91                       commands.
     92         @param telemetry_on_dut: If set, telemetry itself (the test harness)
     93                                  will run on dut.
     94                                  It decides browser=[system|cros-chrome]
     95         """
     96         self._host = host
     97         self._devserver = None
     98         self._telemetry_path = None
     99         self._telemetry_on_dut = telemetry_on_dut
    100         # TODO (llozano crbug.com/324964). Remove conditional code.
    101         # Use a class hierarchy instead.
    102         if local:
    103             self._setup_local_telemetry()
    104         else:
    105             self._setup_devserver_telemetry()
    106 
    107         logging.debug('Telemetry Path: %s', self._telemetry_path)
    108 
    109 
    110     def _setup_devserver_telemetry(self):
    111         """Setup Telemetry to use the devserver."""
    112         logging.debug('Setting up telemetry for devserver testing')
    113         logging.debug('Grabbing build from AFE.')
    114         info = self._host.host_info_store.get()
    115         if not info.build:
    116             logging.error('Unable to locate build label for host: %s.',
    117                           self._host.host_port)
    118             raise error.AutotestError('Failed to grab build for host %s.' %
    119                                       self._host.host_port)
    120 
    121         logging.debug('Setting up telemetry for build: %s', info.build)
    122 
    123         self._devserver = dev_server.ImageServer.resolve(
    124                 info.build, hostname=self._host.hostname)
    125         self._devserver.stage_artifacts(info.build, ['autotest_packages'])
    126         self._telemetry_path = self._devserver.setup_telemetry(build=info.build)
    127 
    128 
    129     def _setup_local_telemetry(self):
    130         """Setup Telemetry to use local path to its sources.
    131 
    132         First look for chrome source root, either externally mounted, or inside
    133         the chroot.  Prefer chrome-src-internal source tree to chrome-src.
    134         """
    135         TELEMETRY_DIR = 'src'
    136         CHROME_LOCAL_SRC = '/var/cache/chromeos-cache/distfiles/target/'
    137         CHROME_EXTERNAL_SRC = os.path.expanduser('~/chrome_root/')
    138 
    139         logging.debug('Setting up telemetry for local testing')
    140 
    141         sources_list = ('chrome-src-internal', 'chrome-src')
    142         dir_list = [CHROME_EXTERNAL_SRC]
    143         dir_list.extend(
    144                 [os.path.join(CHROME_LOCAL_SRC, x) for x in sources_list])
    145         if 'CHROME_ROOT' in os.environ:
    146             dir_list.insert(0, os.environ['CHROME_ROOT'])
    147 
    148         telemetry_src = ''
    149         for dir in dir_list:
    150             if os.path.exists(dir):
    151                 telemetry_src = os.path.join(dir, TELEMETRY_DIR)
    152                 break
    153         else:
    154             raise error.TestError('Telemetry source directory not found.')
    155 
    156         self._devserver = None
    157         self._telemetry_path = telemetry_src
    158 
    159 
    160     def _get_telemetry_cmd(self, script, test_or_benchmark, *args):
    161         """Build command to execute telemetry based on script and benchmark.
    162 
    163         @param script: Telemetry script we want to run. For example:
    164                        [path_to_telemetry_src]/src/tools/telemetry/run_tests.
    165         @param test_or_benchmark: Name of the test or benchmark we want to run,
    166                                   with the page_set (if required) as part of
    167                                   the string.
    168         @param args: additional list of arguments to pass to the script.
    169 
    170         @returns Full telemetry command to execute the script.
    171         """
    172         telemetry_cmd = []
    173         if self._devserver:
    174             devserver_hostname = self._devserver.hostname
    175             telemetry_cmd.extend(['ssh', devserver_hostname])
    176 
    177         if self._telemetry_on_dut:
    178             telemetry_cmd.extend(
    179                     [self._host.ssh_command(alive_interval=900,
    180                                             connection_attempts=4),
    181                      'python',
    182                      script,
    183                      '--verbose',
    184                      '--output-format=chartjson',
    185                      '--output-dir=%s' % DUT_CHROME_ROOT,
    186                      '--browser=system'])
    187         else:
    188             telemetry_cmd.extend(
    189                     ['python',
    190                      script,
    191                      '--verbose',
    192                      '--browser=cros-chrome',
    193                      '--output-format=chartjson',
    194                      '--output-dir=%s' % self._telemetry_path,
    195                      '--remote=%s' % self._host.host_port])
    196         telemetry_cmd.extend(args)
    197         telemetry_cmd.append(test_or_benchmark)
    198 
    199         return ' '.join(telemetry_cmd)
    200 
    201 
    202     def _scp_telemetry_results_cmd(self, perf_results_dir):
    203         """Build command to copy the telemetry results from the devserver.
    204 
    205         @param perf_results_dir: directory path where test output is to be
    206                                  collected.
    207         @returns SCP command to copy the results json to the specified directory.
    208         """
    209         if not perf_results_dir:
    210             return ''
    211 
    212         scp_cmd = ['scp']
    213         if self._telemetry_on_dut:
    214             scp_cmd.append(self._host.make_ssh_options(alive_interval=900,
    215                                                        connection_attempts=4))
    216             if not self._host.is_default_port:
    217                 scp_cmd.append('-P %d' % self._host.port)
    218             src = 'root@%s:%s/results-chart.json' % (self._host.hostname,
    219                                                      DUT_CHROME_ROOT)
    220         else:
    221             devserver_hostname = ''
    222             if self._devserver:
    223                 devserver_hostname = self._devserver.hostname + ':'
    224             src = '%s%s/results-chart.json' % (devserver_hostname,
    225                                                self._telemetry_path)
    226 
    227         scp_cmd.extend([src, perf_results_dir])
    228         return ' '.join(scp_cmd)
    229 
    230 
    231     def _run_cmd(self, cmd):
    232         """Execute an command in a external shell and capture the output.
    233 
    234         @param cmd: String of is a valid shell command.
    235 
    236         @returns The standard out, standard error and the integer exit code of
    237                  the executed command.
    238         """
    239         logging.debug('Running: %s', cmd)
    240 
    241         output = StringIO.StringIO()
    242         error_output = StringIO.StringIO()
    243         exit_code = 0
    244         try:
    245             result = utils.run(cmd, stdout_tee=output,
    246                                stderr_tee=error_output,
    247                                timeout=TELEMETRY_TIMEOUT_MINS*60)
    248             exit_code = result.exit_status
    249         except error.CmdError as e:
    250             logging.debug('Error occurred executing.')
    251             exit_code = e.result_obj.exit_status
    252 
    253         stdout = output.getvalue()
    254         stderr = error_output.getvalue()
    255         logging.debug('Completed with exit code: %d.\nstdout:%s\n'
    256                       'stderr:%s', exit_code, stdout, stderr)
    257         return stdout, stderr, exit_code
    258 
    259 
    260     def _run_telemetry(self, script, test_or_benchmark, *args):
    261         """Runs telemetry on a dut.
    262 
    263         @param script: Telemetry script we want to run. For example:
    264                        [path_to_telemetry_src]/src/tools/telemetry/run_tests.
    265         @param test_or_benchmark: Name of the test or benchmark we want to run,
    266                                  with the page_set (if required) as part of the
    267                                  string.
    268         @param args: additional list of arguments to pass to the script.
    269 
    270         @returns A TelemetryResult Instance with the results of this telemetry
    271                  execution.
    272         """
    273         # TODO (sbasi crbug.com/239933) add support for incognito mode.
    274 
    275         telemetry_cmd = self._get_telemetry_cmd(script,
    276                                                 test_or_benchmark,
    277                                                 *args)
    278         logging.debug('Running Telemetry: %s', telemetry_cmd)
    279 
    280         stdout, stderr, exit_code = self._run_cmd(telemetry_cmd)
    281 
    282         return TelemetryResult(exit_code=exit_code, stdout=stdout,
    283                                stderr=stderr)
    284 
    285 
    286     def _run_scp(self, perf_results_dir):
    287         """Runs telemetry on a dut.
    288 
    289         @param perf_results_dir: The local directory that results are being
    290                                  collected.
    291         """
    292         scp_cmd = self._scp_telemetry_results_cmd(perf_results_dir)
    293         logging.debug('Retrieving Results: %s', scp_cmd)
    294         _, _, exit_code = self._run_cmd(scp_cmd)
    295         if exit_code != 0:
    296             raise error.TestFail('Unable to retrieve results.')
    297 
    298 
    299     def _run_test(self, script, test, *args):
    300         """Runs a telemetry test on a dut.
    301 
    302         @param script: Which telemetry test script we want to run. Can be
    303                        telemetry's base test script or the Chrome OS specific
    304                        test script.
    305         @param test: Telemetry test we want to run.
    306         @param args: additional list of arguments to pass to the script.
    307 
    308         @returns A TelemetryResult Instance with the results of this telemetry
    309                  execution.
    310         """
    311         logging.debug('Running telemetry test: %s', test)
    312         telemetry_script = os.path.join(self._telemetry_path, script)
    313         result = self._run_telemetry(telemetry_script, test, *args)
    314         if result.status is FAILED_STATUS:
    315             raise error.TestFail('Telemetry test %s failed.' % test)
    316         return result
    317 
    318 
    319     def run_telemetry_test(self, test, *args):
    320         """Runs a telemetry test on a dut.
    321 
    322         @param test: Telemetry test we want to run.
    323         @param args: additional list of arguments to pass to the telemetry
    324                      execution script.
    325 
    326         @returns A TelemetryResult Instance with the results of this telemetry
    327                  execution.
    328         """
    329         return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test, *args)
    330 
    331 
    332     def run_telemetry_benchmark(self, benchmark, perf_value_writer=None,
    333                                 *args):
    334         """Runs a telemetry benchmark on a dut.
    335 
    336         @param benchmark: Benchmark we want to run.
    337         @param perf_value_writer: Should be an instance with the function
    338                                   output_perf_value(), if None, no perf value
    339                                   will be written. Typically this will be the
    340                                   job object from an autotest test.
    341         @param args: additional list of arguments to pass to the telemetry
    342                      execution script.
    343 
    344         @returns A TelemetryResult Instance with the results of this telemetry
    345                  execution.
    346         """
    347         logging.debug('Running telemetry benchmark: %s', benchmark)
    348 
    349         if benchmark in ON_DUT_BLACKLIST:
    350             self._telemetry_on_dut = False
    351 
    352         if self._telemetry_on_dut:
    353             telemetry_script = os.path.join(DUT_CHROME_ROOT,
    354                                             TELEMETRY_RUN_BENCHMARKS_SCRIPT)
    355             self._ensure_deps(self._host, benchmark)
    356         else:
    357             telemetry_script = os.path.join(self._telemetry_path,
    358                                             TELEMETRY_RUN_BENCHMARKS_SCRIPT)
    359 
    360         result = self._run_telemetry(telemetry_script, benchmark, *args)
    361 
    362         if result.status is WARNING_STATUS:
    363             raise error.TestWarn('Telemetry Benchmark: %s'
    364                                  ' exited with Warnings.' % benchmark)
    365         if result.status is FAILED_STATUS:
    366             raise error.TestFail('Telemetry Benchmark: %s'
    367                                  ' failed to run.' % benchmark)
    368         if perf_value_writer:
    369             self._run_scp(perf_value_writer.resultsdir)
    370         return result
    371 
    372 
    373     def run_gpu_integration_test(self, test, *args):
    374         """Runs a gpu test on a dut.
    375 
    376         @param test: Gpu test we want to run.
    377         @param args: additional list of arguments to pass to the telemetry
    378                      execution script.
    379 
    380         @returns A TelemetryResult instance with the results of this telemetry
    381                  execution.
    382         """
    383         script = os.path.join(DUT_CHROME_ROOT,
    384                               TELEMETRY_RUN_GPU_TESTS_SCRIPT)
    385         cmd = []
    386         if self._devserver:
    387             devserver_hostname = self._devserver.hostname
    388             cmd.extend(['ssh', devserver_hostname])
    389 
    390         cmd.extend(
    391             [self._host.ssh_command(alive_interval=900, connection_attempts=4),
    392              'python', script])
    393         cmd.extend(args)
    394         cmd.append(test)
    395         cmd = ' '.join(cmd)
    396         stdout, stderr, exit_code = self._run_cmd(cmd)
    397 
    398         if exit_code:
    399             raise error.TestFail('Gpu Integration Test: %s'
    400                                  ' failed to run.' % test)
    401 
    402         return TelemetryResult(exit_code=exit_code, stdout=stdout,
    403                                stderr=stderr)
    404 
    405 
    406     def _ensure_deps(self, dut, test_name):
    407         """
    408         Ensure the dependencies are locally available on DUT.
    409 
    410         @param dut: The autotest host object representing DUT.
    411         @param test_name: Name of the telemetry test.
    412         """
    413         # Get DEPs using host's telemetry.
    414         # Example output, fetch_benchmark_deps.py --output-deps=deps octane:
    415         # {'octane': ['tools/perf/page_sets/data/octane_002.wprgo']}
    416         perf_path = os.path.join(self._telemetry_path, 'tools', 'perf')
    417         deps_path = os.path.join(perf_path, 'fetch_benchmark_deps_result.json')
    418         fetch_path = os.path.join(perf_path, 'fetch_benchmark_deps.py')
    419         format_fetch = ('python %s --output-deps=%s %s')
    420         command_fetch = format_fetch % (fetch_path, deps_path, test_name)
    421         command_get = 'cat %s' % deps_path
    422 
    423         if self._devserver:
    424             devserver_hostname = self._devserver.url().split(
    425                     'http://')[1].split(':')[0]
    426             command_fetch = 'ssh %s %s' % (devserver_hostname, command_fetch)
    427             command_get = 'ssh %s %s' % (devserver_hostname, command_get)
    428 
    429         logging.info('Getting DEPs: %s', command_fetch)
    430         _, _, exit_code = self._run_cmd(command_fetch)
    431         if exit_code != 0:
    432             raise error.TestFail('Error occurred while fetching DEPs.')
    433         stdout, _, exit_code = self._run_cmd(command_get)
    434         if exit_code != 0:
    435             raise error.TestFail('Error occurred while getting DEPs.')
    436 
    437         # Download DEPs to DUT.
    438         # send_file() relies on rsync over ssh. Couldn't be better.
    439         deps = json.loads(stdout)
    440         for dep in deps[test_name]:
    441             src = os.path.join(self._telemetry_path, dep)
    442             dst = os.path.join(DUT_CHROME_ROOT, dep)
    443             if self._devserver:
    444                 logging.info('Copying: %s -> %s', src, dst)
    445                 rsync_cmd = utils.sh_escape('rsync %s %s %s:%s' %
    446                                             (self._host.rsync_options(), src,
    447                                             self._host.hostname, dst))
    448                 utils.run('ssh %s "%s"' % (devserver_hostname, rsync_cmd))
    449             else:
    450                 if not os.path.isfile(src):
    451                     raise error.TestFail('Error occurred while saving DEPs.')
    452                 logging.info('Copying: %s -> %s', src, dst)
    453                 dut.send_file(src, dst)
    454