android_ACTS.py revision 074d2948ced2d50ea30836abd071e71075978d26
1# Copyright 2015 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
8import sys
9
10import common
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib import global_config
13from autotest_lib.client.common_lib.cros import dev_server
14from autotest_lib.server import adb_utils
15from autotest_lib.server import afe_utils
16from autotest_lib.server import constants
17from autotest_lib.server import test
18from autotest_lib.server.cros import dnsname_mangler
19from autotest_lib.site_utils import sponge_utils
20
21CONFIG_FOLDER_LOCATION = global_config.global_config.get_config_value(
22        'ACTS', 'acts_config_folder', default='')
23
24TEST_CONFIG_FILE_FOLDER = os.path.join(CONFIG_FOLDER_LOCATION,
25        'autotest_config')
26TEST_CAMPAIGN_FILE_FOLDER = os.path.join(CONFIG_FOLDER_LOCATION,
27        'autotest_campaign')
28
29DEFAULT_TEST_RELATIVE_LOG_PATH = 'results/logs'
30
31
32class android_ACTS(test.test):
33    """Run an Android CTS test case.
34
35    Component relationship:
36    Workstation ----(ssh)---> TestStation -----(adb)-----> Android DUT
37    This code runs on Workstation.
38    """
39    version = 1
40    acts_result_to_autotest = {
41        'PASS': 'GOOD',
42        'FAIL': 'FAIL',
43        'UNKNOWN': 'WARN',
44        'SKIP': 'ABORT'
45    }
46
47    def push_file_to_teststation(self,
48                                 filename,
49                                 input_path=None,
50                                 output_path=None):
51        """Ensures the file specified by a path exists on test station. If the
52        file specified by input_path does not exist, attempt to locate it in
53        ACTS dirctory.
54
55        @param filename: The name of the file relative to both the input and output
56                         path.
57        @param input_path: The base path on the drone to get the file
58                           from. If none, then the folder in the filename
59                           is used.
60        @param output_path: The base path on the test station to put the file.
61                            By default this is the temp folder.
62
63        @returns: The full path on the test station.
64        """
65        logging.debug('Starting push for %s.', filename)
66
67        if not input_path:
68            input_path = os.path.dirname(os.path.abspath(filename))
69
70        if not output_path:
71            output_path = os.path.join(self.ts_tempfolder, 'configs')
72
73        # Find the path on the source machine
74        full_input_path = os.path.abspath(input_path)
75        if not os.path.exists(full_input_path):
76            raise error.TestError('Invalid input path given %s' % input_path)
77        full_src_file = os.path.join(full_input_path, filename)
78        if not os.path.exists(full_src_file):
79            raise error.TestError(
80                    'Invalid filename, no full path %s exists' % full_src_file)
81
82        # Find the directory part of the file
83        file_dir = os.path.dirname(filename)
84
85        # Find the path on the test station
86        dst_dir = os.path.join(output_path, file_dir)
87
88        logging.info('Pushing file %s to %s.', full_src_file, dst_dir)
89
90        self.test_station.send_file(full_src_file, dst_dir)
91        return os.path.join(output_path, filename)
92
93    def install_sl4a_apk(self):
94        """Installs sl4a on all phones connected to the testbed."""
95        for serial, adb_host in self.testbed.get_adb_devices().iteritems():
96            adb_utils.install_apk_from_build(
97                    adb_host,
98                    constants.SL4A_APK,
99                    constants.SL4A_PACKAGE,
100                    package_name=constants.SL4A_PACKAGE)
101
102    def download_acts(self, download_locaiton=None):
103        """Downloads acts onto a test station.
104
105        Pulls down acts.zip from from devserver and unzips it into the temp
106        directory for this test.
107
108        @param download_locaiton: The directory on the test station to download
109                                  acts into.
110
111        @returns: The base directory for acts.
112        """
113        if not download_locaiton:
114            download_locaiton = self.ts_tempfolder
115
116        host = next(v for v in self.testbed.get_adb_devices().values())
117
118        if not host:
119            raise error.TestError(
120                    'No hosts defined for this test, cannot'
121                    ' determine build to grab artifact from.')
122
123        job_repo_url = afe_utils.get_host_attribute(
124                host, host.job_repo_url_attribute)
125        if not job_repo_url:
126            raise error.TestError('No job repo url defined for this DUT.')
127
128        logging.info('Pulling acts from artifact, url: %s.', job_repo_url)
129
130        devserver_url = dev_server.AndroidBuildServer.get_server_url(
131                job_repo_url)
132        devserver = dev_server.AndroidBuildServer(devserver_url)
133        build_info = host.get_build_info_from_build_url(job_repo_url)
134
135        devserver.trigger_download(
136                build_info['target'],
137                build_info['build_id'],
138                build_info['branch'],
139                files='acts.zip',
140                synchronous=True)
141
142        temp_dir = download_locaiton
143
144        download_dir = '%s/acts' % temp_dir
145
146        logging.info('Downloading from dev server %s to %s',
147                     job_repo_url,
148                     download_dir)
149
150        host.download_file(
151                build_url=job_repo_url,
152                file='acts.zip',
153                dest_dir=temp_dir,
154                unzip=True,
155                unzip_dest=download_dir)
156
157        base_acts_dir = os.path.join(
158                download_dir, 'tools/test/connectivity/acts')
159
160
161        logging.info('ACTs downloaded to %s on test station', base_acts_dir)
162
163        return base_acts_dir
164
165    def setup_configs(self,
166                      config_file,
167                      additional_configs=[],
168                      configs_local_location=None,
169                      configs_remote_location=None):
170        """Setup config files on the test station.
171
172        Takes configuration files and uploads them onto the tests station.
173        Then does any additional setup that is needed.
174
175        @param config_file: The main config for acts to use.
176        @param additional_configs: An additional set of config files to send.
177        @param configs_local_location: Where on the drone are config files
178                                       being found. If none then the default
179                                       location defined in the autotest
180                                       configs is used.
181        @param configs_remote_location: The directory to store configs in,
182                                        by default it is a sub folder of the
183                                        temp folder.
184
185        @returns A list of locations on the test station where the configs where
186                 uploaded. The first element is always the main config.
187        """
188        if not configs_local_location:
189            configs_local_location = TEST_CONFIG_FILE_FOLDER
190
191        if not configs_remote_location:
192            configs_remote_location = os.path.join(
193                    self.ts_tempfolder,
194                    'configs')
195
196        if not config_file:
197            raise error.TestFail('A config file must be specified.')
198
199        logging.info('Pulling configs from %s.', configs_local_location)
200
201        remote_config_file = self.push_file_to_teststation(
202                config_file,
203                input_path=configs_local_location,
204                output_path=configs_remote_location)
205
206        remote_configs = [remote_config_file]
207
208        for additional_config in additional_configs:
209            remote_location = self.push_file_to_teststation(
210                    additional_config,
211                    input_path=configs_local_location,
212                    output_path=configs_remote_location)
213
214            remote_configs.append(remote_location)
215
216        return remote_configs
217
218    def setup_campaign(self,
219                       campaign_file,
220                       campaign_local_location=None,
221                       campagin_remote_location = None):
222        """Sets up campaign files on a test station.
223
224        Will take a local campaign file and upload it to the test station.
225
226        @param campaign_file: The name of the campaign file.
227        @param campaign_local_location: The local directory the campaign file
228                                        is in.
229        @param campagin_remote_location: The remote directory to place the
230                                         campaign file in.
231
232        @returns The remote path to the campaign file.
233        """
234        if not campaign_local_location:
235            campaign_local_location = TEST_CAMPAIGN_FILE_FOLDER
236
237        if not campagin_remote_location:
238            campagin_remote_location = os.path.join(self.ts_tempfolder,
239                                                    'campaigns')
240
241        logging.info('Pulling campaign from %s.', campaign_local_location)
242
243        remote_campaign_file = self.push_file_to_teststation(
244                campaign_file,
245                input_path=campaign_local_location,
246                output_path=campagin_remote_location)
247
248        return remote_campaign_file
249
250
251    def build_environment(self, base_acts_dir=None, log_path=None):
252        """Builds the environment variables for a run.
253
254        @param base_acts_dir: Where acts is stored. If none then the default
255                              acts download location is used.
256        @param log_path: The path to the log file. If none then a log folder
257                         under the temp folder is used.
258
259        @returns: The enviroment variables as a dictionary.
260        """
261        if not log_path:
262            log_path = '%s/%s' % (
263                    self.ts_tempfolder,
264                    DEFAULT_TEST_RELATIVE_LOG_PATH)
265
266        if not base_acts_dir:
267            base_acts_dir = self.acts_download_dir
268
269        framework_dir = os.path.join(base_acts_dir, 'framework')
270        base_test_dir = os.path.join(base_acts_dir, 'tests')
271
272        get_test_paths_result = self.test_station.run(
273                'find %s -type d' % base_test_dir)
274        test_search_dirs = get_test_paths_result.stdout.splitlines()
275
276        env = {'ACTS_TESTPATHS': ':'.join(test_search_dirs),
277               'ACTS_LOGPATH': log_path,
278               'PYTHONPATH': '%s:$PYTHONPATH' % framework_dir}
279
280        logging.info('Enviroment set to: %s', str(env))
281
282        return env
283
284    def run_acts(self,
285                 remote_config_file,
286                 testing_working_dir=None,
287                 remote_acts_file='framework/acts/bin/act.py',
288                 test_file=None,
289                 test_case=None,
290                 env={},
291                 testbed_name=None,
292                 timeout=7200):
293        """Runs ACTs on the test station.
294
295        Runs a test on on the test station and handles logging any details
296        about it during runtime.
297
298        @param remote_config_file: The config file on the test station to use.
299        @param testing_working_dir: The working directory to run acts from.
300                                    By default the acts download location is
301                                    used.
302        @param remote_acts_file: The acts file to use relative from the wokring
303                                 directory.
304        @param test_file: The -tf argument for acts.
305        @param test_case: The tc argument for acts.
306        @param env: The enviroment variables to use with acts.
307        @param testbed_name: The name of the test bed to use, if None then the
308                             default testbed name for the test is used.
309        @param timeout: How long to wait for acts.
310        """
311        if not testbed_name:
312            testbed_name = self.testbed_name
313
314        if not testing_working_dir:
315            testing_working_dir = self.acts_download_dir
316
317        exports = []
318        for key, value in env.items():
319            exports.append('export %s="%s"' % (key, str(value)))
320        env_setup = '; '.join(exports)
321
322        command_setup = 'cd %s' % testing_working_dir
323        act_base_cmd = 'python %s -c %s -tb %s ' % (
324                remote_acts_file, remote_config_file, testbed_name)
325
326        if test_case and test_file:
327            raise ValueError(
328                    'test_case and test_file cannot both have a value.')
329        elif test_case:
330            act_cmd = '%s -tc %s' % (act_base_cmd, test_case)
331        elif test_file:
332            full_test_file = self.setup_campaign(test_file)
333            act_cmd = '%s -tf %s' % (act_base_cmd, full_test_file)
334        else:
335            raise error.TestFail('No tests was specified!')
336
337        command_list = [command_setup, env_setup, act_cmd]
338        full_command = '; '.join(command_list)
339
340        try:
341            logging.debug('Running: %s', full_command)
342            act_result = self.test_station.run(full_command, timeout=timeout)
343            logging.debug('ACTS Output:\n%s', act_result.stdout)
344        except:
345            raise error.TestError('Unexpected error: %s', sys.exc_info())
346
347    def post_act_cmd(self, test_case=None, test_file=None, log_path=None):
348        """Actions to take after act_cmd is finished or failed.
349
350        Actions include collect logs from test station generated by act_cmd
351        and record job results based on `test_run_summary.json`.
352        @param test_case: A string that's passed to act.py's -tc option.
353        @param test_file: A string that's passed to act.py's -tf option.
354
355        @param log_path: The path to where the log output.
356        """
357        logging.info('Running cleanup.')
358        if not log_path:
359            log_path = os.path.join(
360                    self.ts_tempfolder,
361                    DEFAULT_TEST_RELATIVE_LOG_PATH)
362
363        testbed_log_path = os.path.join(log_path, self.testbed_name, 'latest')
364
365        # Following call may fail if logs are not generated by act_cmd yet.
366        # Anyhow, the test must have failed already in that case.
367        self.test_station.get_file(testbed_log_path, self.resultsdir)
368        # Load summary json file.
369        summary_path = os.path.join(
370                self.resultsdir, 'latest', 'test_run_summary.json')
371        # If the test has failed, test_run_summary.json may not exist.
372        if os.path.exists(summary_path):
373            sponge_utils.upload_results_in_test(
374                    self, acts_summary=summary_path)
375            with open(summary_path, 'r') as f:
376                results = json.load(f)['Results']
377            # Report results to Autotest.
378            for result in results:
379                verdict = self.acts_result_to_autotest[result['Result']]
380                details = result['Details']
381                self.job.record(
382                        verdict,
383                        None,
384                        test_case or test_file,
385                        status=(details or ''))
386        else:
387            logging.debug('summary at path %s does not exist!', summary_path)
388
389    def run_once(self,
390                 testbed=None,
391                 config_file=None,
392                 testbed_name=None,
393                 test_case=None,
394                 test_file=None,
395                 additional_configs=[],
396                 acts_timeout=7200):
397        """Run ACTS on the DUT.
398
399        Exactly one of test_case and test_file should be provided.
400
401        @param testbed: Testbed representing the testbed under test. Required.
402        @param config_file: Path to config file locally. Required.
403        @param testbed_name: A string that's passed to act.py's -tb option.
404                             If testbed_name is not provided, set it to the
405                             testbed's hostname without the DNS zone.
406        @param test_case: A string that's passed to act.py's -tc option.
407        @param test_file: A string that's passed to act.py's -tf option.
408        @param additional_configs: A list of paths to be copied over to
409                                   the test station. These files must reside
410                                   in the TEST_CONFIG_FILE_FOLDER.
411        @param acts_timeout: A timeout for the specific ACTS test.
412        """
413        # setup properties
414        self.testbed = testbed
415        if not testbed_name:
416            hostname = testbed.hostname
417            if dnsname_mangler.is_ip_address(hostname):
418                self.testbed_name = hostname
419            else:
420                self.testbed_name = hostname.split('.')[0]
421        else:
422            self.testbed_name = testbed_name
423        self.test_station = testbed.teststation
424        self.ts_tempfolder = self.test_station.get_tmp_dir()
425
426        # install all needed tools
427        self.install_sl4a_apk()
428        self.acts_download_dir = self.download_acts()
429        remote_config_file = self.setup_configs(
430                config_file, additional_configs)
431        env = self.build_environment()
432
433        exception = None
434        try:
435            # launch acts
436            self.run_acts(
437                    remote_config_file[0],
438                    test_file=test_file,
439                    test_case=test_case,
440                    env=env,
441                    timeout=acts_timeout)
442        except Exception, e:
443            logging.error('Unexpected error: %s', sys.exc_info())
444            exception = e
445
446        # cleanup
447        self.post_act_cmd(test_case=test_case, test_file=test_file)
448
449        if exception:
450            raise exception
451