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