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