| #!/usr/bin/env python |
| # |
| # Copyright 2017, The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| Command line utility for running Android tests through TradeFederation. |
| |
| atest helps automate the flow of building test modules across the Android |
| code base and executing the tests via the TradeFederation test harness. |
| |
| atest is designed to support any test types that can be ran by TradeFederation. |
| """ |
| |
| import logging |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| import atest_utils |
| import cli_translator |
| # pylint: disable=import-error |
| import constants |
| import test_runner_handler |
| from test_runners import regression_test_runner |
| |
| EXPECTED_VARS = frozenset([ |
| atest_utils.ANDROID_BUILD_TOP, |
| 'ANDROID_TARGET_OUT_TESTCASES', |
| 'OUT']) |
| BUILD_STEP = 'build' |
| INSTALL_STEP = 'install' |
| TEST_STEP = 'test' |
| ALL_STEPS = [BUILD_STEP, INSTALL_STEP, TEST_STEP] |
| TEST_RUN_DIR_PREFIX = 'atest_run_%s_' |
| HELP_DESC = '''Build, install and run Android tests locally.''' |
| REBUILD_MODULE_INFO_FLAG = '--rebuild-module-info' |
| |
| EPILOG_TEXT = ''' |
| |
| |
| - - - - - - - - - |
| IDENTIFYING TESTS |
| - - - - - - - - - |
| |
| The positional argument <tests> should be a reference to one or more of the |
| tests you'd like to run. Multiple tests can be run in one command by |
| separating test references with spaces. |
| |
| Usage Template: atest <reference_to_test_1> <reference_to_test_2> |
| |
| A <reference_to_test> can be satisfied by the test's MODULE NAME, |
| MODULE:CLASS, CLASS NAME, TF INTEGRATION TEST or FILE PATH. Explanations |
| and examples of each follow. |
| |
| |
| < MODULE NAME > |
| |
| Identifying a test by its module name will run the entire module. Input |
| the name as it appears in the LOCAL_MODULE or LOCAL_PACKAGE_NAME |
| variables in that test's Android.mk or Android.bp file. |
| |
| Note: Use < TF INTEGRATION TEST > to run non-module tests integrated |
| directly into TradeFed. |
| |
| Examples: |
| atest FrameworksServicesTests |
| atest CtsJankDeviceTestCases |
| |
| |
| < MODULE:CLASS > |
| |
| Identifying a test by its class name will run just the tests in that |
| class and not the whole module. MODULE:CLASS is the preferred way to run |
| a single class. MODULE is the same as described above. CLASS is the |
| name of the test class in the .java file. It can either be the fully |
| qualified class name or just the basic name. |
| |
| Examples: |
| atest PtsBatteryTestCases:BatteryTest |
| atest PtsBatteryTestCases:com.google.android.battery.pts.BatteryTest |
| atest CtsJankDeviceTestCases:CtsDeviceJankUi |
| |
| |
| < CLASS NAME > |
| |
| A single class can also be run by referencing the class name without |
| the module name. However, this will take more time than the equivalent |
| MODULE:CLASS reference, so we suggest using a MODULE:CLASS reference |
| whenever possible. |
| |
| Examples: |
| atest ScreenDecorWindowTests |
| atest com.google.android.battery.pts.BatteryTest |
| atest CtsDeviceJankUi |
| |
| |
| < TF INTEGRATION TEST > |
| |
| To run tests that are integrated directly into TradeFed (non-modules), |
| input the name as it appears in the output of the "tradefed.sh list |
| configs" cmd. |
| |
| Examples: |
| atest example/reboot |
| atest native-benchmark |
| |
| |
| < FILE PATH > |
| |
| Both module-based tests and integration-based tests can be run by |
| inputting the path to their test file or dir as appropriate. A single |
| class can also be run by inputting the path to the class's java file. |
| Both relative and absolute paths are supported. |
| |
| Example - run module from android repo root: |
| atest cts/tests/jank/jank |
| |
| Example - same module but from <repo root>/cts/tests/jank: |
| atest . |
| |
| Example - run just class from android repo root: |
| atest cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java |
| |
| Example - run tf integration test from android repo root: |
| atest tools/tradefederation/contrib/res/config/example/reboot.xml |
| |
| |
| - - - - - - - - - - - - - - - - - - - - - - - - - - |
| SPECIFYING INDIVIDUAL STEPS: BUILD, INSTALL OR RUN |
| - - - - - - - - - - - - - - - - - - - - - - - - - - |
| |
| The -b, -i and -t options allow you to specify which steps you want to run. |
| If none of those options are given, then all steps are run. If any of these |
| options are provided then only the listed steps are run. |
| |
| Note: -i alone is not currently support and can only be included with -t. |
| Both -b and -t can be run alone. |
| |
| Examples: |
| atest -b <test> (just build targets) |
| atest -bt <test> (build targets, run tests, but skip installing apk) |
| atest -t <test> (just run test, skip build/install) |
| atest -it <test> (install and run tests, skip building) |
| |
| |
| - - - - - - - - - - - - - |
| RUNNING SPECIFIC METHODS |
| - - - - - - - - - - - - - |
| |
| It is possible to run only specific methods within a test class. To run |
| only specific methods, identify the class in any of the ways supported |
| for identifying a class (MODULE:CLASS, FILE PATH, etc) and then append the |
| name of the method or method using the following template: |
| |
| <reference_to_class>#<method1>,<method2>,<method3>... |
| |
| Examples: |
| FrameworksServicesTests:ScreenDecorWindowTests#testFlagChange,testRemoval |
| com.google.android.battery.pts.BatteryTest#testDischarge |
| |
| |
| - - - - - - - - - - - - - |
| RUNNING MULTIPLE CLASSES |
| - - - - - - - - - - - - - |
| |
| To run multiple classes, deliminate them with spaces just like you would |
| if running multiple tests. Atest will automatically build and run |
| multiple classes in the most efficient way possible. |
| |
| |
| Example - two classes in same module: |
| atest FrameworksServicesTests:ScreenDecorWindowTests FrameworksServicesTest:DimmerTests |
| |
| Example - two classes, different modules: |
| atest FrameworksServicesTests:ScreenDecorWindowTests CtsJankDeviceTestCases:CtsDeviceJankUi |
| |
| |
| - - - - - - - - - - - |
| REGRESSION DETECTION |
| - - - - - - - - - - - |
| |
| Generate pre-patch or post-patch metrics without running regression detection: |
| |
| Example: |
| atest <test> --generate-baseline <optional iter> |
| atest <test> --generate-new-metrics <optional iter> |
| |
| Local regression detection can be run in three options: |
| |
| 1) Provide a folder containing baseline (pre-patch) metrics (generated previously). Atest will |
| run the tests n (default 5) iterations, generate a new set of post-patch metrics, and |
| compare those against existing metrics. |
| |
| Example: |
| atest <test> --detect-regression </path/to/baseline> --generate-new-metrics <optional iter> |
| |
| 2) Provide a folder containing post-patch metrics (generated previously). Atest will run the |
| tests n (default 5) iterations, generate a new set of pre-patch metrics, and compare those |
| against those provided. Note: the developer needs to revert the device/tests to pre-patch |
| state to generate baseline metrics. |
| |
| Example: |
| atest <test> --detect-regression </path/to/new> --generate-baseline <optional iter> |
| |
| 3) Provide 2 folders containing both pre-patch and post-patch metrics. Atest will run no tests |
| but the regression detection algorithm. |
| |
| Example: |
| atest --detect-regression </path/to/baseline> </path/to/new> |
| |
| |
| ''' |
| |
| |
| def _parse_args(argv): |
| """Parse command line arguments. |
| |
| Args: |
| argv: A list of arguments. |
| |
| Returns: |
| An argspace.Namespace class instance holding parsed args. |
| """ |
| import argparse |
| parser = argparse.ArgumentParser( |
| description=HELP_DESC, |
| epilog=EPILOG_TEXT, |
| formatter_class=argparse.RawTextHelpFormatter) |
| parser.add_argument('tests', nargs='*', help='Tests to build and/or run.') |
| parser.add_argument('-b', '--build', action='append_const', dest='steps', |
| const=BUILD_STEP, help='Run a build.') |
| parser.add_argument('-i', '--install', action='append_const', dest='steps', |
| const=INSTALL_STEP, help='Install an APK.') |
| parser.add_argument('-t', '--test', action='append_const', dest='steps', |
| const=TEST_STEP, help='Run the tests.') |
| parser.add_argument('-m', REBUILD_MODULE_INFO_FLAG, action='store_true', |
| help='Forces a rebuild of the module-info.json file. ' |
| 'This may be necessary following a repo sync or ' |
| 'when writing a new test.') |
| parser.add_argument('-w', '--wait-for-debugger', action='store_true', |
| help='Only for instrumentation tests. Waits for ' |
| 'debugger prior to execution.') |
| parser.add_argument('-v', '--verbose', action='store_true', |
| help='Display DEBUG level logging.') |
| parser.add_argument('--generate-baseline', nargs='?', type=int, const=5, default=0, |
| help='Generate baseline metrics, run 5 iterations by default. ' |
| 'Provide an int argument to specify # iterations.') |
| parser.add_argument('--generate-new-metrics', nargs='?', type=int, const=5, default=0, |
| help='Generate new metrics, run 5 iterations by default. ' |
| 'Provide an int argument to specify # iterations.') |
| parser.add_argument('--detect-regression', nargs='*', |
| help='Run regression detection algorithm. Supply ' |
| 'path to baseline and/or new metrics folders.') |
| return parser.parse_args(argv) |
| |
| |
| def _configure_logging(verbose): |
| """Configure the logger. |
| |
| Args: |
| verbose: A boolean. If true display DEBUG level logs. |
| """ |
| if verbose: |
| logging.basicConfig(level=logging.DEBUG) |
| else: |
| logging.basicConfig(level=logging.INFO) |
| |
| |
| def _missing_environment_variables(): |
| """Verify the local environment has been set up to run atest. |
| |
| Returns: |
| List of strings of any missing environment variables. |
| """ |
| missing = filter(None, [x for x in EXPECTED_VARS if not os.environ.get(x)]) |
| if missing: |
| logging.error('Local environment doesn\'t appear to have been ' |
| 'initialized. Did you remember to run lunch? Expected ' |
| 'Environment Variables: %s.', missing) |
| return missing |
| |
| |
| def make_test_run_dir(): |
| """Make the test run dir in tmp. |
| |
| Returns: |
| A string of the dir path. |
| """ |
| utc_epoch_time = int(time.time()) |
| prefix = TEST_RUN_DIR_PREFIX % utc_epoch_time |
| return tempfile.mkdtemp(prefix=prefix) |
| |
| |
| def run_tests(run_commands): |
| """Shell out and execute tradefed run commands. |
| |
| Args: |
| run_commands: A list of strings of Tradefed run commands. |
| """ |
| logging.info('Running tests') |
| # TODO: Build result parser for run command. Until then display raw stdout. |
| for run_command in run_commands: |
| logging.debug('Executing command: %s', run_command) |
| subprocess.check_call(run_command, shell=True, stderr=subprocess.STDOUT) |
| |
| |
| def get_extra_args(args): |
| """Get extra args for test runners. |
| |
| Args: |
| args: arg parsed object. |
| |
| Returns: |
| Dict of extra args for test runners to utilize. |
| """ |
| extra_args = {} |
| if args.wait_for_debugger: |
| extra_args[constants.WAIT_FOR_DEBUGGER] = None |
| steps = args.steps or ALL_STEPS |
| if INSTALL_STEP not in steps: |
| extra_args[constants.DISABLE_INSTALL] = None |
| if args.generate_baseline: |
| extra_args[constants.PRE_PATCH_ITERATIONS] = args.generate_baseline |
| if args.generate_new_metrics: |
| extra_args[constants.POST_PATCH_ITERATIONS] = args.generate_new_metrics |
| return extra_args |
| |
| |
| def _get_regression_detection_args(args, results_dir): |
| """Get args for regression detection test runners. |
| |
| Args: |
| args: parsed args object. |
| results_dir: string directory to store atest results. |
| |
| Returns: |
| Dict of args for regression detection test runner to utilize. |
| """ |
| regression_args = {} |
| pre_patch_folder = (os.path.join(results_dir, 'baseline-metrics') if args.generate_baseline |
| else args.detect_regression.pop(0)) |
| post_patch_folder = (os.path.join(results_dir, 'new-metrics') if args.generate_new_metrics |
| else args.detect_regression.pop(0)) |
| regression_args[constants.PRE_PATCH_FOLDER] = pre_patch_folder |
| regression_args[constants.POST_PATCH_FOLDER] = post_patch_folder |
| return regression_args |
| |
| |
| def _will_run_tests(args): |
| """Determine if there are tests to run. |
| |
| Currently only used by detect_regression to skip the test if just running regression detection. |
| |
| Args: |
| args: parsed args object. |
| |
| Returns: |
| True if there are tests to run, false otherwise. |
| """ |
| return not (args.detect_regression and len(args.detect_regression) == 2) |
| |
| |
| def _has_valid_regression_detection_args(args): |
| """Validate regression detection args. |
| |
| Args: |
| args: parsed args object. |
| |
| Returns: |
| True if args are valid |
| """ |
| if args.generate_baseline and args.generate_new_metrics: |
| logging.error('Cannot collect both baseline and new metrics at the same time.') |
| return False |
| if args.detect_regression is not None: |
| if not args.detect_regression: |
| logging.error('Need to specify at least 1 arg for regression detection.') |
| return False |
| elif len(args.detect_regression) == 1: |
| if args.generate_baseline or args.generate_new_metrics: |
| return True |
| logging.error('Need to specify --generate-baseline or --generate-new-metrics.') |
| return False |
| elif len(args.detect_regression) == 2: |
| if args.generate_baseline: |
| logging.error('Specified 2 metric paths and --generate-baseline, ' |
| 'either drop --generate-baseline or drop a path') |
| return False |
| if args.generate_new_metrics: |
| logging.error('Specified 2 metric paths and --generate-new-metrics, ' |
| 'either drop --generate-new-metrics or drop a path') |
| return False |
| return True |
| else: |
| logging.error('Specified more than 2 metric paths.') |
| return False |
| return True |
| |
| |
| def main(argv): |
| """Entry point of atest script. |
| |
| Args: |
| argv: A list of arguments. |
| |
| Returns: |
| Exit code. |
| """ |
| args = _parse_args(argv) |
| _configure_logging(args.verbose) |
| if _missing_environment_variables(): |
| return constants.EXIT_CODE_ENV_NOT_SETUP |
| if args.generate_baseline and args.generate_new_metrics: |
| logging.error('Cannot collect both baseline and new metrics at the same time.') |
| return constants.EXIT_CODE_ERROR |
| if not _has_valid_regression_detection_args(args): |
| return constants.EXIT_CODE_ERROR |
| repo_root = os.environ.get(atest_utils.ANDROID_BUILD_TOP) |
| results_dir = make_test_run_dir() |
| translator = cli_translator.CLITranslator( |
| results_dir=results_dir, root_dir=repo_root, |
| force_init=args.rebuild_module_info) |
| build_targets = set() |
| test_infos = set() |
| if _will_run_tests(args): |
| try: |
| build_targets, test_infos = translator.translate(args.tests) |
| except cli_translator.TestDiscoveryException: |
| logging.exception('Error occured in test discovery:') |
| logging.info('This can happen after a repo sync or if the test is ' |
| 'new. Running: with "%s" may resolve the issue.', |
| REBUILD_MODULE_INFO_FLAG) |
| return constants.EXIT_CODE_TEST_NOT_FOUND |
| build_targets |= test_runner_handler.get_test_runner_reqs(test_infos) |
| extra_args = get_extra_args(args) |
| if args.detect_regression: |
| build_targets |= (regression_test_runner.RegressionTestRunner('') |
| .get_test_runner_build_reqs()) |
| # args.steps will be None if none of -bit set, else list of params set. |
| steps = args.steps if args.steps else ALL_STEPS |
| if BUILD_STEP in steps: |
| success = atest_utils.build(build_targets, args.verbose) |
| if not success: |
| return constants.EXIT_CODE_BUILD_FAILURE |
| elif TEST_STEP not in steps: |
| logging.warn('Install step without test step currently not ' |
| 'supported, installing AND testing instead.') |
| steps.append(TEST_STEP) |
| if TEST_STEP in steps: |
| test_runner_handler.run_all_tests(results_dir, test_infos, extra_args) |
| if args.detect_regression: |
| regression_args = _get_regression_detection_args(args, results_dir) |
| regression_test_runner.RegressionTestRunner('').run_tests(None, regression_args) |
| return constants.EXIT_CODE_SUCCESS |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |