Home | History | Annotate | Download | only in site_utils
      1 # Copyright 2016 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 
      9 from autotest_lib.client.common_lib import error
     10 from autotest_lib.client.common_lib import global_config
     11 from autotest_lib.server import adb_utils
     12 from autotest_lib.server import constants
     13 from autotest_lib.server.hosts import adb_host
     14 
     15 DEFAULT_ACTS_INTERNAL_DIRECTORY = 'tools/test/connectivity/acts'
     16 
     17 CONFIG_FOLDER_LOCATION = global_config.global_config.get_config_value(
     18     'ACTS', 'acts_config_folder', default='')
     19 
     20 TEST_DIR_NAME = 'tests'
     21 FRAMEWORK_DIR_NAME = 'framework'
     22 SETUP_FILE_NAME = 'setup.py'
     23 CONFIG_DIR_NAME = 'autotest_config'
     24 CAMPAIGN_DIR_NAME = 'autotest_campaign'
     25 LOG_DIR_NAME = 'logs'
     26 ACTS_EXECUTABLE_IN_FRAMEWORK = 'acts/bin/act.py'
     27 
     28 ACTS_TESTPATHS_ENV_KEY = 'ACTS_TESTPATHS'
     29 ACTS_LOGPATH_ENV_KEY = 'ACTS_LOGPATH'
     30 ACTS_PYTHONPATH_ENV_KEY = 'PYTHONPATH'
     31 
     32 
     33 def create_acts_package_from_current_artifact(test_station, job_repo_url,
     34                                               target_zip_file):
     35     """Creates an acts package from the build branch being used.
     36 
     37     Creates an acts artifact from the build branch being used. This is
     38     determined by the job_repo_url passed in.
     39 
     40     @param test_station: The teststation that should be creating the package.
     41     @param job_repo_url: The job_repo_url to get the build info from.
     42     @param target_zip_file: The zip file to create form the artifact on the
     43                             test_station.
     44 
     45     @returns An ActsPackage containing all the information about the zipped
     46              artifact.
     47     """
     48     build_info = adb_host.ADBHost.get_build_info_from_build_url(job_repo_url)
     49 
     50     return create_acts_package_from_artifact(
     51         test_station, build_info['branch'], build_info['target'],
     52         build_info['build_id'], job_repo_url, target_zip_file)
     53 
     54 
     55 def create_acts_package_from_artifact(test_station, branch, target, build_id,
     56                                       devserver, target_zip_file):
     57     """Creates an acts package from a specified branch.
     58 
     59     Grabs the packaged acts artifact from the branch and places it on the
     60     test_station.
     61 
     62     @param test_station: The teststation that should be creating the package.
     63     @param branch: The name of the branch where the artifact is to be pulled.
     64     @param target: The name of the target where the artifact is to be pulled.
     65     @param build_id: The build id to pull the artifact from.
     66     @param devserver: The devserver to use.
     67     @param target_zip_file: The zip file to create on the teststation.
     68 
     69     @returns An ActsPackage containing all the information about the zipped
     70              artifact.
     71     """
     72     devserver.trigger_download(
     73         target, build_id, branch, files='acts.zip', synchronous=True)
     74 
     75     pull_base_url = devserver.get_pull_url(target, build_id, branch)
     76     download_ulr = os.path.join(pull_base_url, 'acts.zip')
     77 
     78     test_station.download_file(download_ulr, target_zip_file)
     79 
     80     return ActsPackage(test_station, target_zip_file)
     81 
     82 
     83 def create_acts_package_from_zip(test_station, zip_location, target_zip_file):
     84     """Creates an acts package from an existing zip.
     85 
     86     Creates an acts package from a zip file that already sits on the drone.
     87 
     88     @param test_station: The teststation to create the package on.
     89     @param zip_location: The location of the zip on the drone.
     90     @param target_zip_file: The zip file to create on the teststaiton.
     91 
     92     @returns An ActsPackage containing all the information about the zipped
     93              artifact.
     94     """
     95     if not os.path.isabs(zip_location):
     96         zip_location = os.path.join(CONFIG_FOLDER_LOCATION, 'acts_artifacts',
     97                                     zip_location)
     98 
     99     test_station.send_file(zip_location, target_zip_file)
    100 
    101     return ActsPackage(test_station, target_zip_file)
    102 
    103 
    104 class ActsPackage(object):
    105     """A packaged version of acts on a teststation."""
    106 
    107     def __init__(self, test_station, zip_file_path):
    108         """
    109         @param test_station: The teststation this package is on.
    110         @param zip_file_path: The path to the zip file on the test station that
    111                               holds the package on the teststation.
    112         """
    113         self.test_station = test_station
    114         self.zip_file = zip_file_path
    115 
    116     def create_container(self,
    117                          container_directory,
    118                          internal_acts_directory=None):
    119         """Unpacks this package into a container.
    120 
    121         Unpacks this acts package into a container to interact with acts.
    122 
    123         @param container_directory: The directory on the teststation to hold
    124                                     the container.
    125         @param internal_acts_directory: The directory inside of the package
    126                                         that holds acts.
    127 
    128         @returns: An ActsContainer with info on the unpacked acts container.
    129         """
    130         self.test_station.run('unzip "%s" -x -d "%s"' %
    131                               (self.zip_file, container_directory))
    132 
    133         return ActsContainer(
    134             self.test_station,
    135             container_directory,
    136             acts_directory=internal_acts_directory)
    137 
    138     def create_environment(self,
    139                            container_directory,
    140                            devices,
    141                            testbed_name,
    142                            internal_acts_directory=None):
    143         """Unpacks this package into an acts testing enviroment.
    144 
    145         Unpacks this acts package into a test enviroment to test with acts.
    146 
    147         @param container_directory: The directory on the teststation to hold
    148                                     the test enviroment.
    149         @param devices: The list of devices in the environment.
    150         @param testbed_name: The name of the testbed.
    151         @param internal_acts_directory: The directory inside of the package
    152                                         that holds acts.
    153 
    154         @returns: An ActsTestingEnvironment with info on the unpacked
    155                   acts testing environment.
    156         """
    157         container = self.create_container(container_directory,
    158                                           internal_acts_directory)
    159 
    160         return ActsTestingEnviroment(
    161             devices=devices,
    162             container=container,
    163             testbed_name=testbed_name)
    164 
    165 
    166 class AndroidTestingEnvironment(object):
    167     """A container for testing android devices on a test station."""
    168 
    169     def __init__(self, devices, testbed_name):
    170         """Creates a new android testing environment.
    171 
    172         @param devices: The devices on the testbed to use.
    173         @param testbed_name: The name for the testbed.
    174         """
    175         self.devices = devices
    176         self.testbed_name = testbed_name
    177 
    178     def install_sl4a_apk(self, force_reinstall=True):
    179         """Install sl4a to all provided devices..
    180 
    181         @param force_reinstall: If true the apk will be force to reinstall.
    182         """
    183         for device in self.devices:
    184             adb_utils.install_apk_from_build(
    185                 device,
    186                 constants.SL4A_APK,
    187                 constants.SL4A_ARTIFACT,
    188                 package_name=constants.SL4A_PACKAGE,
    189                 force_reinstall=force_reinstall)
    190 
    191     def install_apk(self, apk_info, force_reinstall=True):
    192         """Installs an additional apk on all adb devices.
    193 
    194         @param apk_info: A dictionary containing the apk info. This dictionary
    195                          should contain the keys:
    196                             apk="Name of the apk",
    197                             package="Name of the package".
    198                             artifact="Name of the artifact", if missing
    199                                       the package name is used."
    200         @param force_reinstall: If true the apk will be forced to reinstall.
    201         """
    202         for device in self.devices:
    203             adb_utils.install_apk_from_build(
    204                 device,
    205                 apk_info['apk'],
    206                 apk_info.get('artifact') or constants.SL4A_ARTIFACT,
    207                 package_name=apk_info['package'],
    208                 force_reinstall=force_reinstall)
    209 
    210 
    211 class ActsContainer(object):
    212     """A container for working with acts."""
    213 
    214     def __init__(self, test_station, container_directory, acts_directory=None):
    215         """
    216         @param test_station: The test station that the container is on.
    217         @param container_directory: The directory on the teststation this
    218                                     container operates out of.
    219         @param acts_directory: The directory within the container that holds
    220                                acts. If none then it defaults to
    221                                DEFAULT_ACTS_INTERNAL_DIRECTORY.
    222         """
    223         self.test_station = test_station
    224         self.container_directory = container_directory
    225 
    226         if not acts_directory:
    227             acts_directory = DEFAULT_ACTS_INTERNAL_DIRECTORY
    228 
    229         if not os.path.isabs(acts_directory):
    230             self.acts_directory = os.path.join(container_directory,
    231                                                acts_directory)
    232         else:
    233             self.acts_directory = acts_directory
    234 
    235         self.tests_directory = os.path.join(self.acts_directory, TEST_DIR_NAME)
    236         self.framework_directory = os.path.join(self.acts_directory,
    237                                                 FRAMEWORK_DIR_NAME)
    238 
    239         self.acts_file = os.path.join(self.framework_directory,
    240                                       ACTS_EXECUTABLE_IN_FRAMEWORK)
    241 
    242         self.setup_file = os.path.join(self.framework_directory,
    243                                        SETUP_FILE_NAME)
    244 
    245         self.log_directory = os.path.join(container_directory,
    246                                           LOG_DIR_NAME)
    247 
    248         self.config_location = os.path.join(container_directory,
    249                                             CONFIG_DIR_NAME)
    250 
    251         self.acts_file = os.path.join(self.framework_directory,
    252                                       ACTS_EXECUTABLE_IN_FRAMEWORK)
    253 
    254         self.working_directory = os.path.join(container_directory,
    255                                               CONFIG_DIR_NAME)
    256         test_station.run('mkdir %s' % self.working_directory,
    257                          ignore_status=True)
    258 
    259     def get_test_paths(self):
    260         """Get all test paths within this container.
    261 
    262         Gets all paths that hold tests within the container.
    263 
    264         @returns: A list of paths on the teststation that hold tests.
    265         """
    266         get_test_paths_result = self.test_station.run('find %s -type d' %
    267                                                       self.tests_directory)
    268         test_search_dirs = get_test_paths_result.stdout.splitlines()
    269         return test_search_dirs
    270 
    271     def get_python_path(self):
    272         """Get the python path being used.
    273 
    274         Gets the python path that will be set in the enviroment for this
    275         container.
    276 
    277         @returns: A string of the PYTHONPATH enviroment variable to be used.
    278         """
    279         return '%s:$PYTHONPATH' % self.framework_directory
    280 
    281     def get_enviroment(self):
    282         """Gets the enviroment variables to be used for this container.
    283 
    284         @returns: A dictionary of enviroment variables to be used by this
    285                   container.
    286         """
    287         env = {
    288             ACTS_TESTPATHS_ENV_KEY: ':'.join(self.get_test_paths()),
    289             ACTS_LOGPATH_ENV_KEY: self.log_directory,
    290             ACTS_PYTHONPATH_ENV_KEY: self.get_python_path()
    291         }
    292 
    293         return env
    294 
    295     def upload_file(self, src, dst):
    296         """Uploads a file to be used by the container.
    297 
    298         Uploads a file from the drone to the test staiton to be used by the
    299         test container.
    300 
    301         @param src: The source file on the drone. If a relative path is given
    302                     it is assumed to exist in CONFIG_FOLDER_LOCATION.
    303         @param dst: The destination on the teststation. If a relative path is
    304                     given it is assumed that it is within the container.
    305 
    306         @returns: The full path on the teststation.
    307         """
    308         if not os.path.isabs(src):
    309             src = os.path.join(CONFIG_FOLDER_LOCATION, src)
    310 
    311         if not os.path.isabs(dst):
    312             dst = os.path.join(self.container_directory, dst)
    313 
    314         path = os.path.dirname(dst)
    315         self.test_station.run('mkdir "%s"' % path, ignore_status=True)
    316 
    317         original_dst = dst
    318         if os.path.basename(src) == os.path.basename(dst):
    319             dst = os.path.dirname(dst)
    320 
    321         self.test_station.send_file(src, dst)
    322 
    323         return original_dst
    324 
    325 
    326 class ActsTestingEnviroment(AndroidTestingEnvironment):
    327     """A container for running acts tests with a contained version of acts."""
    328 
    329     def __init__(self, container, devices, testbed_name):
    330         """
    331         @param container: The acts container to use.
    332         @param devices: The list of devices to use.
    333         @testbed_name: The name of the testbed being used.
    334         """
    335         super(ActsTestingEnviroment, self).__init__(devices=devices,
    336                                                     testbed_name=testbed_name)
    337 
    338         self.container = container
    339 
    340         self.configs = {}
    341         self.campaigns = {}
    342 
    343     def upload_config(self, config_file):
    344         """Uploads a config file to the container.
    345 
    346         Uploads a config file to the config folder in the container.
    347 
    348         @param config_file: The config file to upload. This must be a file
    349                             within the autotest_config directory under the
    350                             CONFIG_FOLDER_LOCATION.
    351 
    352         @returns: The full path of the config on the test staiton.
    353         """
    354         full_name = os.path.join(CONFIG_DIR_NAME, config_file)
    355 
    356         full_path = self.container.upload_file(full_name, full_name)
    357         self.configs[config_file] = full_path
    358 
    359         return full_path
    360 
    361     def upload_campaign(self, campaign_file):
    362         """Uploads a campaign file to the container.
    363 
    364         Uploads a campaign file to the campaign folder in the container.
    365 
    366         @param campaign_file: The campaign file to upload. This must be a file
    367                               within the autotest_campaign directory under the
    368                               CONFIG_FOLDER_LOCATION.
    369 
    370         @returns: The full path of the campaign on the test staiton.
    371         """
    372         full_name = os.path.join(CAMPAIGN_DIR_NAME, campaign_file)
    373 
    374         full_path = self.container.upload_file(full_name, full_name)
    375         self.campaigns[campaign_file] = full_path
    376 
    377         return full_path
    378 
    379     def setup_enviroment(self, python_bin='python'):
    380         """Sets up the teststation system enviroment so the container can run.
    381 
    382         Prepares the remote system so that the container can run. This involves
    383         uninstalling all versions of acts for the version of python being
    384         used and installing all needed dependencies.
    385 
    386         @param python_bin: The python binary to use.
    387         """
    388         uninstall_command = '%s %s uninstall' % (
    389             python_bin, self.container.setup_file)
    390         install_deps_command = '%s %s install_deps' % (
    391             python_bin, self.container.setup_file)
    392 
    393         self.container.test_station.run(uninstall_command)
    394         self.container.test_station.run(install_deps_command)
    395 
    396     def run_test(self,
    397                  config,
    398                  campaign=None,
    399                  test_case=None,
    400                  extra_env={},
    401                  python_bin='python',
    402                  timeout=7200,
    403                  additional_cmd_line_params=None):
    404         """Runs a test within the container.
    405 
    406         Runs a test within a container using the given settings.
    407 
    408         @param config: The name of the config file to use as the main config.
    409                        This should have already been uploaded with
    410                        upload_config. The string passed into upload_config
    411                        should be used here.
    412         @param campaign: The campaign file to use for this test. If none then
    413                          test_case is assumed. This file should have already
    414                          been uploaded with upload_campaign. The string passed
    415                          into upload_campaign should be used here.
    416         @param test_case: The test case to run the test with. If none then the
    417                           campaign will be used. If multiple are given,
    418                           multiple will be run.
    419         @param extra_env: Extra enviroment variables to run the test with.
    420         @param python_bin: The python binary to execute the test with.
    421         @param timeout: How many seconds to wait before timing out.
    422         @param additional_cmd_line_params: Adds the ability to add any string
    423                                            to the end of the acts.py command
    424                                            line string.  This is intended to
    425                                            add acts command line flags however
    426                                            this is unbounded so it could cause
    427                                            errors if incorrectly set.
    428 
    429         @returns: The results of the test run.
    430         """
    431         if not config in self.configs:
    432             # Check if the config has been uploaded and upload if it hasn't
    433             self.upload_config(config)
    434 
    435         full_config = self.configs[config]
    436 
    437         if campaign:
    438             # When given a campaign check if it's upload.
    439             if not campaign in self.campaigns:
    440                 self.upload_campaign(campaign)
    441 
    442             full_campaign = self.campaigns[campaign]
    443         else:
    444             full_campaign = None
    445 
    446         full_env = self.container.get_enviroment()
    447 
    448         # Setup environment variables.
    449         if extra_env:
    450             for k, v in extra_env.items():
    451                 full_env[k] = extra_env
    452 
    453         logging.info('Using env: %s', full_env)
    454         exports = ('export %s=%s' % (k, v) for k, v in full_env.items())
    455         env_command = ';'.join(exports)
    456 
    457         # Make sure to execute in the working directory.
    458         command_setup = 'cd %s' % self.container.working_directory
    459 
    460         if additional_cmd_line_params:
    461             act_base_cmd = '%s %s -c %s -tb %s %s ' % (
    462                     python_bin, self.container.acts_file, full_config,
    463                     self.testbed_name, additional_cmd_line_params)
    464         else:
    465             act_base_cmd = '%s %s -c %s -tb %s ' % (
    466                     python_bin, self.container.acts_file, full_config,
    467                     self.testbed_name)
    468 
    469         # Format the acts command based on what type of test is being run.
    470         if test_case and campaign:
    471             raise error.TestError(
    472                     'campaign and test_file cannot both have a value.')
    473         elif test_case:
    474             if isinstance(test_case, str):
    475                 test_case = [test_case]
    476             if len(test_case) < 1:
    477                 raise error.TestError('At least one test case must be given.')
    478 
    479             tc_str = ''
    480             for tc in test_case:
    481                 tc_str = '%s %s' % (tc_str, tc)
    482             tc_str = tc_str.strip()
    483 
    484             act_cmd = '%s -tc %s' % (act_base_cmd, tc_str)
    485         elif campaign:
    486             act_cmd = '%s -tf %s' % (act_base_cmd, full_campaign)
    487         else:
    488             raise error.TestFail('No tests was specified!')
    489 
    490         # Format all commands into a single command.
    491         command_list = [command_setup, env_command, act_cmd]
    492         full_command = '; '.join(command_list)
    493 
    494         try:
    495             # Run acts on the remote machine.
    496             act_result = self.container.test_station.run(full_command,
    497                                                          timeout=timeout)
    498             excep = None
    499         except Exception as e:
    500             # Catch any error to store in the results.
    501             act_result = None
    502             excep = e
    503 
    504         return ActsTestResults(str(test_case) or campaign,
    505                                container=self.container,
    506                                devices=self.devices,
    507                                testbed_name=self.testbed_name,
    508                                run_result=act_result,
    509                                exception=excep)
    510 
    511 
    512 class ActsTestResults(object):
    513     """The packaged results of a test run."""
    514     acts_result_to_autotest = {
    515         'PASS': 'GOOD',
    516         'FAIL': 'FAIL',
    517         'UNKNOWN': 'WARN',
    518         'SKIP': 'ABORT'
    519     }
    520 
    521     def __init__(self,
    522                  name,
    523                  container,
    524                  devices,
    525                  testbed_name,
    526                  run_result=None,
    527                  exception=None):
    528         """
    529         @param name: A name to identify the test run.
    530         @param testbed_name: The name the testbed was run with, if none the
    531                              default name of the testbed is used.
    532         @param run_result: The raw i/o result of the test run.
    533         @param log_directory: The directory that acts logged to.
    534         @param exception: An exception that was thrown while running the test.
    535         """
    536         self.name = name
    537         self.run_result = run_result
    538         self.exception = exception
    539         self.log_directory = container.log_directory
    540         self.test_station = container.test_station
    541         self.testbed_name = testbed_name
    542         self.devices = devices
    543 
    544         self.reported_to = set()
    545 
    546         self.json_results = {}
    547         self.results_dir = None
    548         if self.log_directory:
    549             self.results_dir = os.path.join(self.log_directory,
    550                                             self.testbed_name, 'latest')
    551             results_file = os.path.join(self.results_dir,
    552                                         'test_run_summary.json')
    553             cat_log_result = self.test_station.run('cat %s' % results_file,
    554                                                    ignore_status=True)
    555             if not cat_log_result.exit_status:
    556                 self.json_results = json.loads(cat_log_result.stdout)
    557 
    558     def log_output(self):
    559         """Logs the output of the test."""
    560         if self.run_result:
    561             logging.debug('ACTS Output:\n%s', self.run_result.stdout)
    562 
    563     def save_test_info(self, test):
    564         """Save info about the test.
    565 
    566         @param test: The test to save.
    567         """
    568         for device in self.devices:
    569             device.save_info(test.resultsdir)
    570 
    571     def rethrow_exception(self):
    572         """Re-throws the exception thrown during the test."""
    573         if self.exception:
    574             raise self.exception
    575 
    576     def upload_to_local(self, local_dir):
    577         """Saves all acts results to a local directory.
    578 
    579         @param local_dir: The directory on the local machine to save all results
    580                           to.
    581         """
    582         if self.results_dir:
    583             self.test_station.get_file(self.results_dir, local_dir)
    584 
    585     def report_to_autotest(self, test):
    586         """Reports the results to an autotest test object.
    587 
    588         Reports the results to the test and saves all acts results under the
    589         tests results directory.
    590 
    591         @param test: The autotest test object to report to. If this test object
    592                      has already recived our report then this call will be
    593                      ignored.
    594         """
    595         if test in self.reported_to:
    596             return
    597 
    598         if self.results_dir:
    599             self.upload_to_local(test.resultsdir)
    600 
    601         if not 'Results' in self.json_results:
    602             return
    603 
    604         results = self.json_results['Results']
    605         for result in results:
    606             verdict = self.acts_result_to_autotest[result['Result']]
    607             details = result['Details']
    608             test.job.record(verdict, None, self.name, status=(details or ''))
    609 
    610         self.reported_to.add(test)
    611