| # Copyright 2016 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import json |
| import logging |
| import os |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import global_config |
| from autotest_lib.server import adb_utils |
| from autotest_lib.server import constants |
| from autotest_lib.server.hosts import adb_host |
| |
| DEFAULT_ACTS_INTERNAL_DIRECTORY = 'tools/test/connectivity/acts' |
| |
| CONFIG_FOLDER_LOCATION = global_config.global_config.get_config_value( |
| 'ACTS', 'acts_config_folder', default='') |
| |
| TEST_DIR_NAME = 'tests' |
| FRAMEWORK_DIR_NAME = 'framework' |
| SETUP_FILE_NAME = 'setup.py' |
| CONFIG_DIR_NAME = 'autotest_config' |
| CAMPAIGN_DIR_NAME = 'autotest_campaign' |
| LOG_DIR_NAME = 'logs' |
| ACTS_EXECUTABLE_IN_FRAMEWORK = 'acts/bin/act.py' |
| |
| ACTS_TESTPATHS_ENV_KEY = 'ACTS_TESTPATHS' |
| ACTS_LOGPATH_ENV_KEY = 'ACTS_LOGPATH' |
| ACTS_PYTHONPATH_ENV_KEY = 'PYTHONPATH' |
| |
| |
| def create_acts_package_from_current_artifact(test_station, job_repo_url, |
| target_zip_file): |
| """Creates an acts package from the build branch being used. |
| |
| Creates an acts artifact from the build branch being used. This is |
| determined by the job_repo_url passed in. |
| |
| @param test_station: The teststation that should be creating the package. |
| @param job_repo_url: The job_repo_url to get the build info from. |
| @param target_zip_file: The zip file to create form the artifact on the |
| test_station. |
| |
| @returns An ActsPackage containing all the information about the zipped |
| artifact. |
| """ |
| build_info = adb_host.ADBHost.get_build_info_from_build_url(job_repo_url) |
| |
| return create_acts_package_from_artifact( |
| test_station, build_info['branch'], build_info['target'], |
| build_info['build_id'], job_repo_url, target_zip_file) |
| |
| |
| def create_acts_package_from_artifact(test_station, branch, target, build_id, |
| devserver, target_zip_file): |
| """Creates an acts package from a specified branch. |
| |
| Grabs the packaged acts artifact from the branch and places it on the |
| test_station. |
| |
| @param test_station: The teststation that should be creating the package. |
| @param branch: The name of the branch where the artifact is to be pulled. |
| @param target: The name of the target where the artifact is to be pulled. |
| @param build_id: The build id to pull the artifact from. |
| @param devserver: The devserver to use. |
| @param target_zip_file: The zip file to create on the teststation. |
| |
| @returns An ActsPackage containing all the information about the zipped |
| artifact. |
| """ |
| devserver.trigger_download( |
| target, build_id, branch, files='acts.zip', synchronous=True) |
| |
| pull_base_url = devserver.get_pull_url(target, build_id, branch) |
| download_ulr = os.path.join(pull_base_url, 'acts.zip') |
| |
| test_station.download_file(download_ulr, target_zip_file) |
| |
| return ActsPackage(test_station, target_zip_file) |
| |
| |
| def create_acts_package_from_zip(test_station, zip_location, target_zip_file): |
| """Creates an acts package from an existing zip. |
| |
| Creates an acts package from a zip file that already sits on the drone. |
| |
| @param test_station: The teststation to create the package on. |
| @param zip_location: The location of the zip on the drone. |
| @param target_zip_file: The zip file to create on the teststaiton. |
| |
| @returns An ActsPackage containing all the information about the zipped |
| artifact. |
| """ |
| if not os.path.isabs(zip_location): |
| zip_location = os.path.join(CONFIG_FOLDER_LOCATION, 'acts_artifacts', |
| zip_location) |
| |
| test_station.send_file(zip_location, target_zip_file) |
| |
| return ActsPackage(test_station, target_zip_file) |
| |
| |
| class ActsPackage(object): |
| """A packaged version of acts on a teststation.""" |
| |
| def __init__(self, test_station, zip_file_path): |
| """ |
| @param test_station: The teststation this package is on. |
| @param zip_file_path: The path to the zip file on the test station that |
| holds the package on the teststation. |
| """ |
| self.test_station = test_station |
| self.zip_file = zip_file_path |
| |
| def create_container(self, |
| container_directory, |
| internal_acts_directory=None): |
| """Unpacks this package into a container. |
| |
| Unpacks this acts package into a container to interact with acts. |
| |
| @param container_directory: The directory on the teststation to hold |
| the container. |
| @param internal_acts_directory: The directory inside of the package |
| that holds acts. |
| |
| @returns: An ActsContainer with info on the unpacked acts container. |
| """ |
| self.test_station.run('unzip "%s" -x -d "%s"' % |
| (self.zip_file, container_directory)) |
| |
| return ActsContainer( |
| self.test_station, |
| container_directory, |
| acts_directory=internal_acts_directory) |
| |
| def create_environment(self, |
| container_directory, |
| devices, |
| testbed_name, |
| internal_acts_directory=None): |
| """Unpacks this package into an acts testing enviroment. |
| |
| Unpacks this acts package into a test enviroment to test with acts. |
| |
| @param container_directory: The directory on the teststation to hold |
| the test enviroment. |
| @param devices: The list of devices in the environment. |
| @param testbed_name: The name of the testbed. |
| @param internal_acts_directory: The directory inside of the package |
| that holds acts. |
| |
| @returns: An ActsTestingEnvironment with info on the unpacked |
| acts testing environment. |
| """ |
| container = self.create_container(container_directory, |
| internal_acts_directory) |
| |
| return ActsTestingEnviroment( |
| devices=devices, |
| container=container, |
| testbed_name=testbed_name) |
| |
| |
| class AndroidTestingEnvironment(object): |
| """A container for testing android devices on a test station.""" |
| |
| def __init__(self, devices, testbed_name): |
| """Creates a new android testing environment. |
| |
| @param devices: The devices on the testbed to use. |
| @param testbed_name: The name for the testbed. |
| """ |
| self.devices = devices |
| self.testbed_name = testbed_name |
| |
| def install_sl4a_apk(self, force_reinstall=True): |
| """Install sl4a to all provided devices.. |
| |
| @param force_reinstall: If true the apk will be force to reinstall. |
| """ |
| for device in self.devices: |
| adb_utils.install_apk_from_build( |
| device, |
| constants.SL4A_APK, |
| constants.SL4A_ARTIFACT, |
| package_name=constants.SL4A_PACKAGE, |
| force_reinstall=force_reinstall) |
| |
| def install_apk(self, apk_info, force_reinstall=True): |
| """Installs an additional apk on all adb devices. |
| |
| @param apk_info: A dictionary containing the apk info. This dictionary |
| should contain the keys: |
| apk="Name of the apk", |
| package="Name of the package". |
| artifact="Name of the artifact", if missing |
| the package name is used." |
| @param force_reinstall: If true the apk will be forced to reinstall. |
| """ |
| for device in self.devices: |
| adb_utils.install_apk_from_build( |
| device, |
| apk_info['apk'], |
| apk_info.get('artifact') or constants.SL4A_ARTIFACT, |
| package_name=apk_info['package'], |
| force_reinstall=force_reinstall) |
| |
| |
| class ActsContainer(object): |
| """A container for working with acts.""" |
| |
| def __init__(self, test_station, container_directory, acts_directory=None): |
| """ |
| @param test_station: The test station that the container is on. |
| @param container_directory: The directory on the teststation this |
| container operates out of. |
| @param acts_directory: The directory within the container that holds |
| acts. If none then it defaults to |
| DEFAULT_ACTS_INTERNAL_DIRECTORY. |
| """ |
| self.test_station = test_station |
| self.container_directory = container_directory |
| |
| if not acts_directory: |
| acts_directory = DEFAULT_ACTS_INTERNAL_DIRECTORY |
| |
| if not os.path.isabs(acts_directory): |
| self.acts_directory = os.path.join(container_directory, |
| acts_directory) |
| else: |
| self.acts_directory = acts_directory |
| |
| self.tests_directory = os.path.join(self.acts_directory, TEST_DIR_NAME) |
| self.framework_directory = os.path.join(self.acts_directory, |
| FRAMEWORK_DIR_NAME) |
| |
| self.acts_file = os.path.join(self.framework_directory, |
| ACTS_EXECUTABLE_IN_FRAMEWORK) |
| |
| self.setup_file = os.path.join(self.framework_directory, |
| SETUP_FILE_NAME) |
| |
| self.log_directory = os.path.join(container_directory, |
| LOG_DIR_NAME) |
| |
| self.config_location = os.path.join(container_directory, |
| CONFIG_DIR_NAME) |
| |
| self.acts_file = os.path.join(self.framework_directory, |
| ACTS_EXECUTABLE_IN_FRAMEWORK) |
| |
| self.working_directory = os.path.join(container_directory, |
| CONFIG_DIR_NAME) |
| test_station.run('mkdir %s' % self.working_directory, |
| ignore_status=True) |
| |
| def get_test_paths(self): |
| """Get all test paths within this container. |
| |
| Gets all paths that hold tests within the container. |
| |
| @returns: A list of paths on the teststation that hold tests. |
| """ |
| get_test_paths_result = self.test_station.run('find %s -type d' % |
| self.tests_directory) |
| test_search_dirs = get_test_paths_result.stdout.splitlines() |
| return test_search_dirs |
| |
| def get_python_path(self): |
| """Get the python path being used. |
| |
| Gets the python path that will be set in the enviroment for this |
| container. |
| |
| @returns: A string of the PYTHONPATH enviroment variable to be used. |
| """ |
| return '%s:$PYTHONPATH' % self.framework_directory |
| |
| def get_enviroment(self): |
| """Gets the enviroment variables to be used for this container. |
| |
| @returns: A dictionary of enviroment variables to be used by this |
| container. |
| """ |
| env = { |
| ACTS_TESTPATHS_ENV_KEY: ':'.join(self.get_test_paths()), |
| ACTS_LOGPATH_ENV_KEY: self.log_directory, |
| ACTS_PYTHONPATH_ENV_KEY: self.get_python_path() |
| } |
| |
| return env |
| |
| def upload_file(self, src, dst): |
| """Uploads a file to be used by the container. |
| |
| Uploads a file from the drone to the test staiton to be used by the |
| test container. |
| |
| @param src: The source file on the drone. If a relative path is given |
| it is assumed to exist in CONFIG_FOLDER_LOCATION. |
| @param dst: The destination on the teststation. If a relative path is |
| given it is assumed that it is within the container. |
| |
| @returns: The full path on the teststation. |
| """ |
| if not os.path.isabs(src): |
| src = os.path.join(CONFIG_FOLDER_LOCATION, src) |
| |
| if not os.path.isabs(dst): |
| dst = os.path.join(self.container_directory, dst) |
| |
| path = os.path.dirname(dst) |
| self.test_station.run('mkdir "%s"' % path, ignore_status=True) |
| |
| original_dst = dst |
| if os.path.basename(src) == os.path.basename(dst): |
| dst = os.path.dirname(dst) |
| |
| self.test_station.send_file(src, dst) |
| |
| return original_dst |
| |
| |
| class ActsTestingEnviroment(AndroidTestingEnvironment): |
| """A container for running acts tests with a contained version of acts.""" |
| |
| def __init__(self, container, devices, testbed_name): |
| """ |
| @param container: The acts container to use. |
| @param devices: The list of devices to use. |
| @testbed_name: The name of the testbed being used. |
| """ |
| super(ActsTestingEnviroment, self).__init__(devices=devices, |
| testbed_name=testbed_name) |
| |
| self.container = container |
| |
| self.configs = {} |
| self.campaigns = {} |
| |
| def upload_config(self, config_file): |
| """Uploads a config file to the container. |
| |
| Uploads a config file to the config folder in the container. |
| |
| @param config_file: The config file to upload. This must be a file |
| within the autotest_config directory under the |
| CONFIG_FOLDER_LOCATION. |
| |
| @returns: The full path of the config on the test staiton. |
| """ |
| full_name = os.path.join(CONFIG_DIR_NAME, config_file) |
| |
| full_path = self.container.upload_file(full_name, full_name) |
| self.configs[config_file] = full_path |
| |
| return full_path |
| |
| def upload_campaign(self, campaign_file): |
| """Uploads a campaign file to the container. |
| |
| Uploads a campaign file to the campaign folder in the container. |
| |
| @param campaign_file: The campaign file to upload. This must be a file |
| within the autotest_campaign directory under the |
| CONFIG_FOLDER_LOCATION. |
| |
| @returns: The full path of the campaign on the test staiton. |
| """ |
| full_name = os.path.join(CAMPAIGN_DIR_NAME, campaign_file) |
| |
| full_path = self.container.upload_file(full_name, full_name) |
| self.campaigns[campaign_file] = full_path |
| |
| return full_path |
| |
| def setup_enviroment(self, python_bin='python'): |
| """Sets up the teststation system enviroment so the container can run. |
| |
| Prepares the remote system so that the container can run. This involves |
| uninstalling all versions of acts for the version of python being |
| used and installing all needed dependencies. |
| |
| @param python_bin: The python binary to use. |
| """ |
| uninstall_command = '%s %s uninstall' % ( |
| python_bin, self.container.setup_file) |
| install_deps_command = '%s %s install_deps' % ( |
| python_bin, self.container.setup_file) |
| |
| self.container.test_station.run(uninstall_command) |
| self.container.test_station.run(install_deps_command) |
| |
| def run_test(self, |
| config, |
| campaign=None, |
| test_case=None, |
| extra_env={}, |
| python_bin='python', |
| timeout=7200, |
| additional_cmd_line_params=None): |
| """Runs a test within the container. |
| |
| Runs a test within a container using the given settings. |
| |
| @param config: The name of the config file to use as the main config. |
| This should have already been uploaded with |
| upload_config. The string passed into upload_config |
| should be used here. |
| @param campaign: The campaign file to use for this test. If none then |
| test_case is assumed. This file should have already |
| been uploaded with upload_campaign. The string passed |
| into upload_campaign should be used here. |
| @param test_case: The test case to run the test with. If none then the |
| campaign will be used. If multiple are given, |
| multiple will be run. |
| @param extra_env: Extra enviroment variables to run the test with. |
| @param python_bin: The python binary to execute the test with. |
| @param timeout: How many seconds to wait before timing out. |
| @param additional_cmd_line_params: Adds the ability to add any string |
| to the end of the acts.py command |
| line string. This is intended to |
| add acts command line flags however |
| this is unbounded so it could cause |
| errors if incorrectly set. |
| |
| @returns: The results of the test run. |
| """ |
| if not config in self.configs: |
| # Check if the config has been uploaded and upload if it hasn't |
| self.upload_config(config) |
| |
| full_config = self.configs[config] |
| |
| if campaign: |
| # When given a campaign check if it's upload. |
| if not campaign in self.campaigns: |
| self.upload_campaign(campaign) |
| |
| full_campaign = self.campaigns[campaign] |
| else: |
| full_campaign = None |
| |
| full_env = self.container.get_enviroment() |
| |
| # Setup environment variables. |
| if extra_env: |
| for k, v in extra_env.items(): |
| full_env[k] = extra_env |
| |
| logging.info('Using env: %s', full_env) |
| exports = ('export %s=%s' % (k, v) for k, v in full_env.items()) |
| env_command = ';'.join(exports) |
| |
| # Make sure to execute in the working directory. |
| command_setup = 'cd %s' % self.container.working_directory |
| |
| if additional_cmd_line_params: |
| act_base_cmd = '%s %s -c %s -tb %s %s ' % ( |
| python_bin, self.container.acts_file, full_config, |
| self.testbed_name, additional_cmd_line_params) |
| else: |
| act_base_cmd = '%s %s -c %s -tb %s ' % ( |
| python_bin, self.container.acts_file, full_config, |
| self.testbed_name) |
| |
| # Format the acts command based on what type of test is being run. |
| if test_case and campaign: |
| raise error.TestError( |
| 'campaign and test_file cannot both have a value.') |
| elif test_case: |
| if isinstance(test_case, str): |
| test_case = [test_case] |
| if len(test_case) < 1: |
| raise error.TestError('At least one test case must be given.') |
| |
| tc_str = '' |
| for tc in test_case: |
| tc_str = '%s %s' % (tc_str, tc) |
| tc_str = tc_str.strip() |
| |
| act_cmd = '%s -tc %s' % (act_base_cmd, tc_str) |
| elif campaign: |
| act_cmd = '%s -tf %s' % (act_base_cmd, full_campaign) |
| else: |
| raise error.TestFail('No tests was specified!') |
| |
| # Format all commands into a single command. |
| command_list = [command_setup, env_command, act_cmd] |
| full_command = '; '.join(command_list) |
| |
| try: |
| # Run acts on the remote machine. |
| act_result = self.container.test_station.run(full_command, |
| timeout=timeout) |
| excep = None |
| except Exception as e: |
| # Catch any error to store in the results. |
| act_result = None |
| excep = e |
| |
| return ActsTestResults(str(test_case) or campaign, |
| container=self.container, |
| devices=self.devices, |
| testbed_name=self.testbed_name, |
| run_result=act_result, |
| exception=excep) |
| |
| |
| class ActsTestResults(object): |
| """The packaged results of a test run.""" |
| acts_result_to_autotest = { |
| 'PASS': 'GOOD', |
| 'FAIL': 'FAIL', |
| 'UNKNOWN': 'WARN', |
| 'SKIP': 'ABORT' |
| } |
| |
| def __init__(self, |
| name, |
| container, |
| devices, |
| testbed_name, |
| run_result=None, |
| exception=None): |
| """ |
| @param name: A name to identify the test run. |
| @param testbed_name: The name the testbed was run with, if none the |
| default name of the testbed is used. |
| @param run_result: The raw i/o result of the test run. |
| @param log_directory: The directory that acts logged to. |
| @param exception: An exception that was thrown while running the test. |
| """ |
| self.name = name |
| self.run_result = run_result |
| self.exception = exception |
| self.log_directory = container.log_directory |
| self.test_station = container.test_station |
| self.testbed_name = testbed_name |
| self.devices = devices |
| |
| self.reported_to = set() |
| |
| self.json_results = {} |
| self.results_dir = None |
| if self.log_directory: |
| self.results_dir = os.path.join(self.log_directory, |
| self.testbed_name, 'latest') |
| results_file = os.path.join(self.results_dir, |
| 'test_run_summary.json') |
| cat_log_result = self.test_station.run('cat %s' % results_file, |
| ignore_status=True) |
| if not cat_log_result.exit_status: |
| self.json_results = json.loads(cat_log_result.stdout) |
| |
| def log_output(self): |
| """Logs the output of the test.""" |
| if self.run_result: |
| logging.debug('ACTS Output:\n%s', self.run_result.stdout) |
| |
| def save_test_info(self, test): |
| """Save info about the test. |
| |
| @param test: The test to save. |
| """ |
| for device in self.devices: |
| device.save_info(test.resultsdir) |
| |
| def rethrow_exception(self): |
| """Re-throws the exception thrown during the test.""" |
| if self.exception: |
| raise self.exception |
| |
| def upload_to_local(self, local_dir): |
| """Saves all acts results to a local directory. |
| |
| @param local_dir: The directory on the local machine to save all results |
| to. |
| """ |
| if self.results_dir: |
| self.test_station.get_file(self.results_dir, local_dir) |
| |
| def report_to_autotest(self, test): |
| """Reports the results to an autotest test object. |
| |
| Reports the results to the test and saves all acts results under the |
| tests results directory. |
| |
| @param test: The autotest test object to report to. If this test object |
| has already recived our report then this call will be |
| ignored. |
| """ |
| if test in self.reported_to: |
| return |
| |
| if self.results_dir: |
| self.upload_to_local(test.resultsdir) |
| |
| if not 'Results' in self.json_results: |
| return |
| |
| results = self.json_results['Results'] |
| for result in results: |
| verdict = self.acts_result_to_autotest[result['Result']] |
| details = result['Details'] |
| test.job.record(verdict, None, self.name, status=(details or '')) |
| |
| self.reported_to.add(test) |