1#!/usr/bin/env python3 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16import logging 17from builtins import str 18 19import argparse 20import multiprocessing 21import signal 22import sys 23import traceback 24 25from acts import config_parser 26from acts import keys 27from acts import signals 28from acts import test_runner 29from acts.config.config_generator import ConfigGenerator 30 31 32def _run_test(parsed_config, test_identifiers, repeat=1): 33 """Instantiate and runs test_runner.TestRunner. 34 35 This is the function to start separate processes with. 36 37 Args: 38 parsed_config: A dict that is a set of configs for one 39 test_runner.TestRunner. 40 test_identifiers: A list of tuples, each identifies what test case to 41 run on what test class. 42 repeat: Number of times to iterate the specified tests. 43 44 Returns: 45 True if all tests passed without any error, False otherwise. 46 """ 47 runner = _create_test_runner(parsed_config, test_identifiers) 48 try: 49 for i in range(repeat): 50 runner.run() 51 return runner.results.is_all_pass 52 except signals.TestAbortAll: 53 return True 54 except: 55 print("Exception when executing %s, iteration %s." % 56 (runner.testbed_name, i)) 57 print(traceback.format_exc()) 58 finally: 59 runner.stop() 60 61 62def _create_test_runner(parsed_config, test_identifiers): 63 """Instantiates one test_runner.TestRunner object and register termination 64 signal handlers that properly shut down the test_runner.TestRunner run. 65 66 Args: 67 parsed_config: A dict that is a set of configs for one 68 test_runner.TestRunner. 69 test_identifiers: A list of tuples, each identifies what test case to 70 run on what test class. 71 72 Returns: 73 A test_runner.TestRunner object. 74 """ 75 try: 76 t = test_runner.TestRunner(parsed_config, test_identifiers) 77 except: 78 print("Failed to instantiate test runner, abort.") 79 print(traceback.format_exc()) 80 sys.exit(1) 81 # Register handler for termination signals. 82 handler = config_parser.gen_term_signal_handler([t]) 83 signal.signal(signal.SIGTERM, handler) 84 signal.signal(signal.SIGINT, handler) 85 return t 86 87 88def _run_tests_parallel(parsed_configs, test_identifiers, repeat): 89 """Executes requested tests in parallel. 90 91 Each test run will be in its own process. 92 93 Args: 94 parsed_configs: A list of dicts, each is a set of configs for one 95 test_runner.TestRunner. 96 test_identifiers: A list of tuples, each identifies what test case to 97 run on what test class. 98 repeat: Number of times to iterate the specified tests. 99 100 Returns: 101 True if all test runs executed successfully, False otherwise. 102 """ 103 print("Executing {} concurrent test runs.".format(len(parsed_configs))) 104 arg_list = [(c, test_identifiers, repeat) for c in parsed_configs] 105 results = [] 106 with multiprocessing.Pool(processes=len(parsed_configs)) as pool: 107 # Can't use starmap for py2 compatibility. One day, one day...... 108 for args in arg_list: 109 results.append(pool.apply_async(_run_test, args)) 110 pool.close() 111 pool.join() 112 for r in results: 113 if r.get() is False or isinstance(r, Exception): 114 return False 115 116 117def _run_tests_sequential(parsed_configs, test_identifiers, repeat): 118 """Executes requested tests sequentially. 119 120 Requested test runs will commence one after another according to the order 121 of their corresponding configs. 122 123 Args: 124 parsed_configs: A list of dicts, each is a set of configs for one 125 test_runner.TestRunner. 126 test_identifiers: A list of tuples, each identifies what test case to 127 run on what test class. 128 repeat: Number of times to iterate the specified tests. 129 130 Returns: 131 True if all test runs executed successfully, False otherwise. 132 """ 133 ok = True 134 for c in parsed_configs: 135 try: 136 ret = _run_test(c, test_identifiers, repeat) 137 ok = ok and ret 138 except Exception as e: 139 print("Exception occurred when executing test bed %s. %s" % 140 (c[keys.Config.key_testbed.value], e)) 141 return ok 142 143 144def main(argv): 145 """This is a sample implementation of a cli entry point for ACTS test 146 execution. 147 148 Or you could implement your own cli entry point using acts.config_parser 149 functions and acts.test_runner.execute_one_test_class. 150 """ 151 parser = argparse.ArgumentParser( 152 description=("Specify tests to run. If nothing specified, " 153 "run all test cases found.")) 154 parser.add_argument( 155 '-c', 156 '--config', 157 nargs=1, 158 type=str, 159 required=True, 160 metavar="<PATH>", 161 help="Path to the test configuration file.") 162 parser.add_argument( 163 '--test_args', 164 nargs='+', 165 type=str, 166 metavar="Arg1 Arg2 ...", 167 help=("Command-line arguments to be passed to every test case in a " 168 "test run. Use with caution.")) 169 parser.add_argument( 170 '-p', 171 '--parallel', 172 action="store_true", 173 help=("If set, tests will be executed on all testbeds in parallel. " 174 "Otherwise, tests are executed iteratively testbed by testbed.")) 175 parser.add_argument( 176 '-ci', 177 '--campaign_iterations', 178 metavar="<CAMPAIGN_ITERATIONS>", 179 nargs='?', 180 type=int, 181 const=1, 182 default=1, 183 help="Number of times to run the campaign or a group of test cases.") 184 parser.add_argument( 185 '-tb', 186 '--testbed', 187 nargs='+', 188 type=str, 189 metavar="[<TEST BED NAME1> <TEST BED NAME2> ...]", 190 help="Specify which test beds to run tests on.") 191 parser.add_argument( 192 '-lp', 193 '--logpath', 194 type=str, 195 metavar="<PATH>", 196 help="Root path under which all logs will be placed.") 197 parser.add_argument( 198 '-tp', 199 '--testpaths', 200 nargs='*', 201 type=str, 202 metavar="<PATH> <PATH>", 203 help="One or more non-recursive test class search paths.") 204 205 group = parser.add_mutually_exclusive_group(required=True) 206 group.add_argument( 207 '-tc', 208 '--testclass', 209 nargs='+', 210 type=str, 211 metavar="[TestClass1 TestClass2:test_xxx ...]", 212 help="A list of test classes/cases to run.") 213 group.add_argument( 214 '-tf', 215 '--testfile', 216 nargs=1, 217 type=str, 218 metavar="<PATH>", 219 help=("Path to a file containing a comma delimited list of test " 220 "classes to run.")) 221 parser.add_argument( 222 '-r', 223 '--random', 224 action="store_true", 225 help="If set, tests will be executed in random order.") 226 parser.add_argument( 227 '-ti', 228 '--test_case_iterations', 229 metavar="<TEST_CASE_ITERATIONS>", 230 nargs='?', 231 type=int, 232 help="Number of times to run every test case.") 233 234 args = parser.parse_args(argv) 235 test_list = None 236 if args.testfile: 237 test_list = config_parser.parse_test_file(args.testfile[0]) 238 elif args.testclass: 239 test_list = args.testclass 240 parsed_configs = config_parser.load_test_config_file( 241 args.config[0], args.testbed, args.testpaths, args.logpath, 242 args.test_args, args.random, args.test_case_iterations) 243 244 log = logging.getLogger() 245 try: 246 new_parsed_args = ConfigGenerator().generate_configs() 247 248 for old_config, new_config in zip(parsed_configs, new_parsed_args): 249 if not old_config.items() <= new_config.items(): 250 log.warning('Backward compat broken:\n%s\n%s' % ( 251 old_config, new_config)) 252 except SystemExit as e: 253 log.warning('Unable to parse command line flags: %s' % 254 traceback.format_exc(e)) 255 except Exception as e: 256 log.warning('Failed to generate configs through the new system: ' 257 '%s' % traceback.format_exc(e)) 258 259 # Prepare args for test runs 260 test_identifiers = config_parser.parse_test_list(test_list) 261 262 # Execute test runners. 263 if args.parallel and len(parsed_configs) > 1: 264 print('Running tests in parallel.') 265 exec_result = _run_tests_parallel(parsed_configs, test_identifiers, 266 args.campaign_iterations) 267 else: 268 print('Running tests sequentially.') 269 exec_result = _run_tests_sequential(parsed_configs, test_identifiers, 270 args.campaign_iterations) 271 if exec_result is False: 272 # return 1 upon test failure. 273 sys.exit(1) 274 sys.exit(0) 275 276 277if __name__ == "__main__": 278 main(sys.argv[1:]) 279