mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2016 The Chromium Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 7 | """Runs the CTS test APKs stored in CIPD.""" |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 8 | |
| 9 | import argparse |
| 10 | import json |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 11 | import logging |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 12 | import os |
| 13 | import shutil |
| 14 | import sys |
| 15 | import tempfile |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 16 | import zipfile |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 17 | |
| 18 | |
| 19 | sys.path.append(os.path.join( |
| 20 | os.path.dirname(__file__), os.pardir, os.pardir, 'build', 'android')) |
| 21 | import devil_chromium # pylint: disable=import-error |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 22 | from devil.android.sdk import version_codes # pylint: disable=import-error |
| 23 | from devil.android.tools import script_common # pylint: disable=import-error |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 24 | from devil.utils import cmd_helper # pylint: disable=import-error |
| 25 | |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 26 | # cts test archives for all platforms are stored in this bucket |
| 27 | # contents need to be updated if there is an important fix to any of |
| 28 | # the tests |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 29 | |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 30 | _TEST_RUNNER_PATH = os.path.join( |
| 31 | os.path.dirname(__file__), os.pardir, os.pardir, |
| 32 | 'build', 'android', 'test_runner.py') |
| 33 | |
| 34 | _EXPECTED_FAILURES_FILE = os.path.join( |
| 35 | os.path.dirname(__file__), 'cts_config', 'expected_failure_on_bot.json') |
| 36 | _WEBVIEW_CTS_GCS_PATH_FILE = os.path.join( |
| 37 | os.path.dirname(__file__), 'cts_config', 'webview_cts_gcs_path.json') |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 38 | _CTS_ARCHIVE_DIR = os.path.join(os.path.dirname(__file__), 'cts_archive') |
| 39 | |
| 40 | _SDK_PLATFORM_DICT = { |
| 41 | version_codes.LOLLIPOP: 'L', |
| 42 | version_codes.LOLLIPOP_MR1: 'L', |
| 43 | version_codes.MARSHMALLOW: 'M', |
| 44 | version_codes.NOUGAT: 'N', |
| 45 | version_codes.NOUGAT_MR1: 'N', |
| 46 | version_codes.OREO: 'O', |
| 47 | version_codes.OREO_MR1: 'O' |
| 48 | } |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 49 | |
| 50 | |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 51 | def GetCtsInfo(arch, platform, item): |
| 52 | """Gets contents of CTS Info for arch and platform. |
| 53 | |
| 54 | See _WEBVIEW_CTS_GCS_PATH_FILE |
| 55 | """ |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 56 | with open(_WEBVIEW_CTS_GCS_PATH_FILE) as f: |
| 57 | cts_gcs_path_info = json.load(f) |
| 58 | try: |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 59 | return cts_gcs_path_info[arch][platform][item] |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 60 | except KeyError: |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 61 | raise Exception('No %s info available for arch:%s, android:%s' % |
| 62 | (item, arch, platform)) |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 63 | |
| 64 | |
| 65 | def GetExpectedFailures(): |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 66 | """Gets list of tests expected to fail in <class>#<method> format. |
| 67 | |
| 68 | See _EXPECTED_FAILURES_FILE |
| 69 | """ |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 70 | with open(_EXPECTED_FAILURES_FILE) as f: |
| 71 | expected_failures_info = json.load(f) |
| 72 | expected_failures = [] |
| 73 | for class_name, methods in expected_failures_info.iteritems(): |
| 74 | expected_failures.extend(['%s#%s' % (class_name, m['name']) |
| 75 | for m in methods]) |
| 76 | return expected_failures |
| 77 | |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 78 | def GetTestRunFilterArg(test_run, skip_expected_failures): |
| 79 | skips = [] |
| 80 | if skip_expected_failures: |
| 81 | skips = GetExpectedFailures() |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 82 | |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 83 | excludes = test_run.get("excludes", []) |
| 84 | includes = test_run.get("includes", []) |
| 85 | assert len(excludes) == 0 or len(includes) == 0, \ |
| 86 | "test_runs error, can't have both includes and excludes: %s" % test_run |
| 87 | if len(includes) > 0: |
| 88 | return ['-f=' + ':'.join([i["match"] for i in includes])] |
| 89 | else: |
| 90 | skips.extend([i["match"] for i in excludes]) |
| 91 | if len(skips) > 0: |
| 92 | return ['-f=' + "-" + ':'.join(skips)] |
| 93 | return [] |
| 94 | |
| 95 | def RunCTS(test_runner_args, local_cts_dir, test_run, |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 96 | skip_expected_failures=True, json_results_file=None): |
| 97 | """Run tests in apk using test_runner script at _TEST_RUNNER_PATH. |
| 98 | |
| 99 | Returns the script result code, |
| 100 | tests expected to fail will be skipped unless skip_expected_failures |
| 101 | is set to False, test results will be stored in |
| 102 | the json_results_file file if specified |
| 103 | """ |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 104 | |
| 105 | apk = test_run['apk'] |
| 106 | |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 107 | local_test_runner_args = test_runner_args + ['--test-apk', |
| 108 | os.path.join(local_cts_dir, apk)] |
| 109 | |
| 110 | # TODO(mikecase): This doesn't work at all with the |
| 111 | # --gtest-filter test runner option currently. The |
| 112 | # filter options will just override eachother. |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 113 | # The preferred method is to specify test filters per release in |
| 114 | # the CTS_GCS path file. It will override any |
| 115 | # previous filters, including ones in expected failures |
| 116 | # file. |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 117 | local_test_runner_args.extend(GetTestRunFilterArg(test_run, |
| 118 | skip_expected_failures)) |
| 119 | |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 120 | if json_results_file: |
| 121 | local_test_runner_args += ['--json-results-file=%s' % |
| 122 | json_results_file] |
| 123 | return cmd_helper.RunCmd( |
| 124 | [_TEST_RUNNER_PATH, 'instrumentation'] + local_test_runner_args) |
| 125 | |
| 126 | |
| 127 | def MergeTestResults(existing_results_json, additional_results_json): |
| 128 | """Appends results in additional_results_json to existing_results_json.""" |
| 129 | for k, v in additional_results_json.iteritems(): |
| 130 | if k not in existing_results_json: |
| 131 | existing_results_json[k] = v |
| 132 | else: |
Ian Vollick | 2698a26 | 2018-07-31 13:59:57 | [diff] [blame] | 133 | if isinstance(v, dict): |
| 134 | if not isinstance(existing_results_json[k], dict): |
| 135 | raise NotImplementedError( |
| 136 | "Can't merge results field %s of different types" % v) |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 137 | existing_results_json[k].update(v) |
Ian Vollick | 2698a26 | 2018-07-31 13:59:57 | [diff] [blame] | 138 | elif isinstance(v, list): |
| 139 | if not isinstance(existing_results_json[k], list): |
| 140 | raise NotImplementedError( |
| 141 | "Can't merge results field %s of different types" % v) |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 142 | existing_results_json[k].extend(v) |
| 143 | else: |
| 144 | raise NotImplementedError( |
| 145 | "Can't merge results field %s that is not a list or dict" % v) |
| 146 | |
| 147 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 148 | def ExtractCTSZip(args): |
| 149 | """Extract the CTS tests for args.platform. |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 150 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 151 | Extract the CTS zip file from _CTS_ARCHIVE_DIR to |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 152 | apk_dir if specified, or a new temporary directory if not. |
| 153 | Returns following tuple (local_cts_dir, base_cts_dir, delete_cts_dir): |
| 154 | local_cts_dir - CTS extraction location for current arch and platform |
| 155 | base_cts_dir - Root directory for all the arches and platforms |
| 156 | delete_cts_dir - Set if the base_cts_dir was created as a temporary |
| 157 | directory |
| 158 | """ |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 159 | base_cts_dir = None |
| 160 | delete_cts_dir = False |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 161 | relative_cts_zip_path = GetCtsInfo(args.arch, args.platform, 'filename') |
| 162 | |
| 163 | if args.apk_dir: |
| 164 | base_cts_dir = args.apk_dir |
| 165 | else: |
| 166 | base_cts_dir = tempfile.mkdtemp() |
| 167 | delete_cts_dir = True |
| 168 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 169 | cts_zip_path = os.path.join(_CTS_ARCHIVE_DIR, relative_cts_zip_path) |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 170 | local_cts_dir = os.path.join(base_cts_dir, |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 171 | GetCtsInfo(args.arch, args.platform, |
| 172 | 'unzip_dir') |
| 173 | ) |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 174 | zf = zipfile.ZipFile(cts_zip_path, 'r') |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 175 | zf.extractall(local_cts_dir) |
| 176 | return (local_cts_dir, base_cts_dir, delete_cts_dir) |
| 177 | |
| 178 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 179 | def RunAllCTSTests(args, test_runner_args): |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 180 | """Run CTS tests downloaded from _CTS_BUCKET. |
| 181 | |
| 182 | Downloads CTS tests from bucket, runs them for the |
| 183 | specified platform+arch, then creates a single |
| 184 | results json file (if specified) |
| 185 | Returns 0 if all tests passed, otherwise |
| 186 | returns the failure code of the last failing |
| 187 | test. |
| 188 | """ |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 189 | local_cts_dir, base_cts_dir, delete_cts_dir = ExtractCTSZip(args) |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 190 | cts_result = 0 |
| 191 | json_results_file = args.json_results_file |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 192 | try: |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 193 | cts_test_runs = GetCtsInfo(args.arch, args.platform, 'test_runs') |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 194 | cts_results_json = {} |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 195 | for cts_test_run in cts_test_runs: |
| 196 | iteration_cts_result = 0 |
| 197 | if json_results_file: |
| 198 | with tempfile.NamedTemporaryFile() as iteration_json_file: |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 199 | iteration_cts_result = RunCTS(test_runner_args, local_cts_dir, |
Andrew Luo | 6a84e61 | 2018-11-08 04:44:44 | [diff] [blame] | 200 | cts_test_run, |
| 201 | args.skip_expected_failures, |
| 202 | iteration_json_file.name) |
| 203 | with open(iteration_json_file.name) as f: |
| 204 | additional_results_json = json.load(f) |
| 205 | MergeTestResults(cts_results_json, additional_results_json) |
| 206 | else: |
| 207 | iteration_cts_result = RunCTS(test_runner_args, local_cts_dir, |
| 208 | cts_test_run, |
| 209 | args.skip_expected_failures) |
| 210 | if iteration_cts_result: |
| 211 | cts_result = iteration_cts_result |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 212 | if json_results_file: |
| 213 | with open(json_results_file, 'w') as f: |
| 214 | json.dump(cts_results_json, f, indent=2) |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 215 | finally: |
| 216 | if delete_cts_dir and base_cts_dir: |
| 217 | shutil.rmtree(base_cts_dir) |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 218 | |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 219 | return cts_result |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 220 | |
| 221 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 222 | def DeterminePlatform(device): |
| 223 | """Determines the platform based on the Android SDK level |
| 224 | |
| 225 | Returns the first letter of the platform in uppercase |
| 226 | if platform is found, otherwise returns None |
| 227 | """ |
| 228 | return _SDK_PLATFORM_DICT.get(device.build_version_sdk) |
| 229 | |
| 230 | |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 231 | def main(): |
| 232 | parser = argparse.ArgumentParser() |
| 233 | parser.add_argument( |
| 234 | '--arch', |
Mike Case | 34800816 | 2017-06-27 18:35:29 | [diff] [blame] | 235 | choices=['arm64'], |
| 236 | default='arm64', |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 237 | help='Arch for CTS tests.') |
| 238 | parser.add_argument( |
| 239 | '--platform', |
Andrew Luo | 6924b1b5 | 2018-04-20 18:37:11 | [diff] [blame] | 240 | choices=['L', 'M', 'N', 'O'], |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 241 | required=False, |
| 242 | default=None, |
| 243 | help='Android platform version for CTS tests. ' |
| 244 | 'Will auto-determine based on SDK level by default.') |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 245 | parser.add_argument( |
| 246 | '--skip-expected-failures', |
| 247 | action='store_true', |
| 248 | help='Option to skip all tests that are expected to fail.') |
| 249 | parser.add_argument( |
| 250 | '--apk-dir', |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 251 | help='Directory to extract CTS APKs to. ' |
| 252 | 'Will use temp directory by default.') |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 253 | parser.add_argument( |
| 254 | '--test-launcher-summary-output', |
| 255 | '--json-results-file', |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 256 | '--write-full-results-to', |
Andrew Luo | bb25061 | 2018-10-04 21:46:21 | [diff] [blame] | 257 | '--isolated-script-test-output', |
Andrew Luo | 8a4f19ed | 2018-04-19 20:07:47 | [diff] [blame] | 258 | dest='json_results_file', type=os.path.realpath, |
| 259 | help='If set, will dump results in JSON form to the specified file. ' |
| 260 | 'Note that this will also trigger saving per-test logcats to ' |
| 261 | 'logdog.') |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 262 | script_common.AddDeviceArguments(parser) |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 263 | |
| 264 | args, test_runner_args = parser.parse_known_args() |
| 265 | devil_chromium.Initialize() |
| 266 | |
Andrew Luo | 96e2fef8d | 2018-08-22 20:18:24 | [diff] [blame] | 267 | devices = script_common.GetDevices(args.devices, args.blacklist_file) |
| 268 | if len(devices) > 1: |
| 269 | logging.warning('Only single device supported, using 1st of %d devices: %s', |
| 270 | len(devices), devices[0].serial) |
| 271 | test_runner_args.extend(['-d', devices[0].serial]) |
| 272 | |
| 273 | if args.platform is None: |
| 274 | args.platform = DeterminePlatform(devices[0]) |
| 275 | if args.platform is None: |
| 276 | raise Exception('Could not auto-determine device platform, ' |
| 277 | 'please specifiy --platform') |
| 278 | |
| 279 | return RunAllCTSTests(args, test_runner_args) |
mikecase | 5c5e3f0 | 2016-12-14 21:54:21 | [diff] [blame] | 280 | |
| 281 | |
| 282 | if __name__ == '__main__': |
| 283 | sys.exit(main()) |