普通文本  |  611行  |  23.17 KB

# 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)