• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import json
6import logging
7import os
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib import global_config
11from autotest_lib.server import adb_utils
12from autotest_lib.server import constants
13from autotest_lib.server.hosts import adb_host
14
15DEFAULT_ACTS_INTERNAL_DIRECTORY = 'tools/test/connectivity/acts'
16
17CONFIG_FOLDER_LOCATION = global_config.global_config.get_config_value(
18    'ACTS', 'acts_config_folder', default='')
19
20TEST_DIR_NAME = 'tests'
21FRAMEWORK_DIR_NAME = 'framework'
22SETUP_FILE_NAME = 'setup.py'
23CONFIG_DIR_NAME = 'autotest_config'
24CAMPAIGN_DIR_NAME = 'autotest_campaign'
25LOG_DIR_NAME = 'logs'
26ACTS_EXECUTABLE_IN_FRAMEWORK = 'acts/bin/act.py'
27
28ACTS_TESTPATHS_ENV_KEY = 'ACTS_TESTPATHS'
29ACTS_LOGPATH_ENV_KEY = 'ACTS_LOGPATH'
30ACTS_PYTHONPATH_ENV_KEY = 'PYTHONPATH'
31
32
33def 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
55def 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
83def 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
104class 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
166class 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
211class 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
326class 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
512class 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