Kuang-che Wu | 1fcc022 | 2018-07-07 16:43:22 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python2 |
| 2 | # -*- coding: utf-8 -*- |
| 3 | # Copyright 2018 The Chromium OS 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 | """Diagnose ChromeOS autotest regressions. |
| 7 | |
| 8 | This is integrated bisection utility. Given ChromeOS, Chrome, Android source |
Kuang-che Wu | 927231f | 2018-07-24 14:21:56 +0800 | [diff] [blame] | 9 | tree, and necessary parameters, this script can determine which components to |
Kuang-che Wu | 1fcc022 | 2018-07-07 16:43:22 +0800 | [diff] [blame] | 10 | bisect, and hopefully output the culprit CL of regression. |
| 11 | |
| 12 | Sometimes the script failed to figure out the final CL for various reasons, it |
| 13 | will cut down the search range as narrow as it can. |
| 14 | """ |
| 15 | from __future__ import print_function |
| 16 | import argparse |
| 17 | import fnmatch |
| 18 | import glob |
| 19 | import logging |
| 20 | import os |
| 21 | |
| 22 | from bisect_kit import cli |
| 23 | from bisect_kit import common |
| 24 | from bisect_kit import configure |
| 25 | from bisect_kit import core |
| 26 | from bisect_kit import cros_lab_util |
| 27 | from bisect_kit import cros_util |
| 28 | from bisect_kit import diagnoser_cros |
| 29 | from bisect_kit import util |
| 30 | |
| 31 | logger = logging.getLogger(__name__) |
| 32 | |
| 33 | # What chrome binaries to build for given autotest. |
| 34 | # This dict is created manually by inspecting output of |
| 35 | # 'grep -r ChromeBinaryTest autotest/files/client/site_tests' |
| 36 | # If you change this dict, build_and_deploy_chrome_helper.sh may need update |
| 37 | # as well. |
| 38 | CHROME_BINARIES_OF_TEST = { |
| 39 | 'graphics_Chrome.ozone_gl_unittests': ['ozone_gl_unittests'], |
| 40 | 'security_SandboxLinuxUnittests': ['sandbox_linux_unittests'], |
| 41 | 'video_HangoutHardwarePerf*': [ |
| 42 | 'video_decode_accelerator_unittest', |
| 43 | 'video_encode_accelerator_unittest', |
| 44 | ], |
| 45 | 'video_JDAPerf*': ['jpeg_decode_accelerator_unittest'], |
| 46 | 'video_JEAPerf': ['jpeg_encode_accelerator_unittest'], |
| 47 | 'video_JpegDecodeAccelerator': ['jpeg_decode_accelerator_unittest'], |
| 48 | 'video_JpegEncodeAccelerator': ['jpeg_encode_accelerator_unittest'], |
| 49 | 'video_VDAPerf': ['video_decode_accelerator_unittest'], |
| 50 | 'video_VDASanity': ['video_decode_accelerator_unittest'], |
| 51 | 'video_VEAPerf': ['video_encode_accelerator_unittest'], |
| 52 | 'video_VideoDecodeAccelerator*': ['video_decode_accelerator_unittest'], |
| 53 | 'video_VideoEncodeAccelerator*': ['video_encode_accelerator_unittest'], |
| 54 | } |
| 55 | |
| 56 | |
| 57 | def create_argument_parser(): |
| 58 | parser = argparse.ArgumentParser() |
| 59 | common.add_common_arguments(parser) |
| 60 | parser.add_argument('--session_base', default='bisect.sessions') |
| 61 | parser.add_argument('--session', help='Session name', required=True) |
| 62 | |
| 63 | group = parser.add_argument_group(title='Essential options') |
| 64 | group.add_argument( |
| 65 | '--chromeos_root', |
| 66 | metavar='CHROMEOS_ROOT', |
| 67 | type=cli.argtype_dir_path, |
| 68 | required=True, |
| 69 | default=configure.get('CHROMEOS_ROOT'), |
| 70 | help='ChromeOS tree root') |
| 71 | group.add_argument( |
Kuang-che Wu | 94f48e5 | 2018-07-25 15:28:31 +0800 | [diff] [blame] | 72 | '--chromeos_repo_mirror_dir', |
| 73 | type=cli.argtype_dir_path, |
| 74 | default=configure.get('CHROMEOS_REPO_MIRROR_DIR', ''), |
| 75 | help='ChromeOS repo mirror path') |
| 76 | group.add_argument( |
Kuang-che Wu | 1fcc022 | 2018-07-07 16:43:22 +0800 | [diff] [blame] | 77 | '--android_root', |
| 78 | metavar='ANDROID_ROOT', |
| 79 | type=cli.argtype_dir_path, |
| 80 | required=True, |
| 81 | default=configure.get('ANDROID_ROOT'), |
| 82 | help='Android tree root') |
| 83 | group.add_argument( |
| 84 | '--android_repo_mirror_dir', |
| 85 | type=cli.argtype_dir_path, |
| 86 | required=True, |
| 87 | default=configure.get('ANDROID_REPO_MIRROR_DIR'), |
| 88 | help='Android repo mirror path') |
| 89 | group.add_argument( |
| 90 | '--chrome_root', |
| 91 | metavar='CHROME_ROOT', |
| 92 | type=cli.argtype_dir_path, |
| 93 | required=True, |
| 94 | default=configure.get('CHROME_ROOT'), |
| 95 | help='Chrome tree root') |
| 96 | group.add_argument( |
Kuang-che Wu | 94f48e5 | 2018-07-25 15:28:31 +0800 | [diff] [blame] | 97 | '--gclient_cache_dir', |
| 98 | required=True, |
| 99 | metavar='GCLIENT_CACHE_DIR', |
| 100 | type=cli.argtype_dir_path, |
| 101 | default=configure.get('GCLIENT_CACHE_DIR'), |
| 102 | help='gclient cache dir') |
| 103 | group.add_argument( |
Kuang-che Wu | 1fcc022 | 2018-07-07 16:43:22 +0800 | [diff] [blame] | 104 | '--old', |
| 105 | type=cros_util.argtype_cros_version, |
| 106 | required=True, |
| 107 | help='ChromeOS version with old behavior') |
| 108 | group.add_argument( |
| 109 | '--new', |
| 110 | type=cros_util.argtype_cros_version, |
| 111 | required=True, |
| 112 | help='ChromeOS version with new behavior') |
| 113 | group.add_argument('test_name', help='Test name') |
| 114 | |
| 115 | group = parser.add_argument_group(title='DUT allocation options') |
| 116 | group.add_argument( |
| 117 | 'dut', |
| 118 | metavar='DUT', |
| 119 | help='Address of DUT (Device Under Test). If "%s", DUT will be ' |
| 120 | 'automatically allocated from the lab' % cros_lab_util.LAB_DUT) |
| 121 | group.add_argument( |
| 122 | '--board', |
| 123 | metavar='BOARD', |
| 124 | default=configure.get('BOARD'), |
| 125 | help='ChromeOS board name') |
| 126 | group.add_argument( |
| 127 | '--sku', |
| 128 | metavar='SKU', |
| 129 | help='SKU criteria if DUT is auto allocated from the lab') |
| 130 | |
| 131 | group = parser.add_argument_group(title='Options for benchmark test') |
| 132 | group.add_argument('--metric', help='Metric name of benchmark test') |
| 133 | group.add_argument( |
| 134 | '--old_value', type=float, help='For benchmark test, old value of metric') |
| 135 | group.add_argument( |
| 136 | '--new_value', type=float, help='For benchmark test, new value of metric') |
| 137 | |
| 138 | group = parser.add_argument_group(title='Options passed to test_that') |
| 139 | group.add_argument( |
| 140 | '--args', |
| 141 | help='Extra args passed to "test_that --args"; Overrides the default') |
| 142 | |
| 143 | group = parser.add_argument_group(title='Bisect behavior options') |
| 144 | group.add_argument( |
| 145 | '--noisy', |
| 146 | help='Enable noisy binary search. Example value: "old=1/10,new=2/3"') |
| 147 | group.add_argument( |
| 148 | '--always_reflash', |
| 149 | action='store_true', |
| 150 | help='Do not trust ChromeOS version number of DUT and always reflash. ' |
| 151 | 'This is usually only needed when resume because previous bisect was ' |
| 152 | 'interrupted and the DUT may be in an unexpected state') |
| 153 | |
| 154 | return parser |
| 155 | |
| 156 | |
| 157 | def grab_dut(opts): |
| 158 | # Assume "DEPENDENCIES" is identical between the period of |
| 159 | # `old` and `new` version. |
| 160 | autotest_dir = os.path.join(opts.chromeos_root, |
| 161 | cros_util.prebuilt_autotest_dir) |
| 162 | info = cros_util.get_autotest_test_info(autotest_dir, opts.test_name) |
| 163 | assert info |
| 164 | |
| 165 | extra_labels = [] |
| 166 | dependencies = info.variables.get('DEPENDENCIES', '') |
| 167 | for label in dependencies.split(','): |
| 168 | label = label.strip() |
| 169 | # Skip non-machine labels |
| 170 | if label in ['cleanup-reboot']: |
| 171 | continue |
| 172 | extra_labels.append(label) |
| 173 | |
| 174 | reason = 'bisect-kit: %s' % opts.session |
| 175 | host = cros_lab_util.allocate_host(reason, opts.board, opts.sku, extra_labels) |
| 176 | if not host: |
| 177 | logger.error('unable to allocate dut') |
| 178 | return None |
| 179 | |
| 180 | logger.info('allocated host %s', host) |
| 181 | return host |
| 182 | |
| 183 | |
| 184 | def may_depend_on_extra_chrome_binaries(autotest_dir, test_name): |
| 185 | info = cros_util.get_autotest_test_info(autotest_dir, test_name) |
| 186 | assert info |
| 187 | dirpath = os.path.dirname(info.path) |
| 188 | for pypath in glob.glob(os.path.join(dirpath, '*.py')): |
| 189 | if 'ChromeBinaryTest' in open(pypath).read(): |
| 190 | return True |
| 191 | return False |
| 192 | |
| 193 | |
| 194 | def determine_chrome_binaries(opts): |
| 195 | chrome_binaries = None |
| 196 | for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items(): |
| 197 | if fnmatch.fnmatch(opts.test_name, name_pattern): |
| 198 | chrome_binaries = binaries |
| 199 | break |
| 200 | |
| 201 | autotest_dir = os.path.join(opts.chromeos_root, |
| 202 | cros_util.prebuilt_autotest_dir) |
| 203 | if chrome_binaries: |
| 204 | logger.info('This test depends on chrome binary: %s', chrome_binaries) |
| 205 | elif may_depend_on_extra_chrome_binaries(autotest_dir, opts.test_name): |
| 206 | logger.warning('%s code used ChromeBinaryTest but the binary is unknown; ' |
| 207 | 'please update CHROME_BINARIES_OF_TEST table', |
| 208 | opts.test_name) |
| 209 | return chrome_binaries |
| 210 | |
| 211 | |
| 212 | def check_options(opts, parser): |
| 213 | if opts.dut == cros_lab_util.LAB_DUT: |
| 214 | session_dir = os.path.join(opts.session_base, opts.session) |
| 215 | # 1 is log file |
| 216 | if os.path.exists(session_dir) and len(os.listdir(session_dir)) > 1: |
| 217 | parser.error('Do not resume if the DUT is dynamically allocated; ' |
| 218 | '%s is not empty' % session_dir) |
| 219 | |
| 220 | if not opts.board: |
| 221 | if opts.dut == cros_lab_util.LAB_DUT: |
| 222 | parser.error( |
| 223 | '--board need to be specified if DUT is "%s"' % cros_lab_util.LAB_DUT) |
| 224 | else: |
| 225 | opts.board = cros_util.query_dut_board(opts.dut) |
| 226 | |
| 227 | if cros_util.is_cros_short_version(opts.old): |
| 228 | opts.old = cros_util.version_to_full(opts.board, opts.old) |
| 229 | if cros_util.is_cros_short_version(opts.new): |
| 230 | opts.new = cros_util.version_to_full(opts.board, opts.new) |
| 231 | |
| 232 | if opts.metric: |
| 233 | if opts.old_value is None: |
| 234 | parser.error('--old_value is not provided') |
| 235 | if opts.new_value is None: |
| 236 | parser.error('--new_value is not provided') |
| 237 | else: |
| 238 | if opts.old_value is not None: |
| 239 | parser.error('--old_value is provided but --metric is not') |
| 240 | if opts.new_value is not None: |
| 241 | parser.error('--new_value is provided but --metric is not') |
| 242 | |
| 243 | |
| 244 | def main(args=None): |
| 245 | common.init() |
| 246 | parser = create_argument_parser() |
| 247 | opts = parser.parse_args(args) |
| 248 | common.config_logging(opts) |
| 249 | check_options(opts, parser) |
| 250 | |
| 251 | # prebuilt version will be specified later. |
| 252 | common_switch_cmd = [ |
| 253 | './switch_autotest_prebuilt.py', |
| 254 | '--chromeos_root', opts.chromeos_root, |
| 255 | '--test_name', opts.test_name, |
| 256 | '--board', opts.board, |
| 257 | ] # yapf: disable |
| 258 | |
| 259 | common_eval_cmd = [ |
| 260 | './eval_cros_autotest.py', |
| 261 | '--chromeos_root', opts.chromeos_root, |
| 262 | '--test_name', opts.test_name, |
| 263 | ] # yapf: disable |
| 264 | if opts.metric: |
| 265 | common_eval_cmd += [ |
| 266 | '--metric', opts.metric, |
| 267 | '--old_value', str(opts.old_value), |
| 268 | '--new_value', str(opts.new_value), |
| 269 | ] # yapf: disable |
| 270 | if opts.args: |
| 271 | common_eval_cmd += ['--args', opts.args] |
| 272 | |
| 273 | # Unpack old autotest prebuilt, assume following information don't change |
| 274 | # between versions: |
| 275 | # - what chrome binaries to run |
| 276 | # - dependency labels for DUT allocation |
| 277 | util.check_call(*(common_switch_cmd + [opts.old])) |
| 278 | |
| 279 | chrome_binaries = determine_chrome_binaries(opts) |
| 280 | |
| 281 | with cros_lab_util.dut_manager(opts.dut, lambda: grab_dut(opts)) as dut: |
| 282 | if not dut: |
| 283 | raise core.ExecutionFatalError('unable to allocate DUT') |
| 284 | assert cros_util.is_dut(dut) |
| 285 | opts.dut = dut |
| 286 | common_eval_cmd.append(opts.dut) |
| 287 | |
| 288 | diagnoser = diagnoser_cros.CrosDiagnoser( |
Kuang-che Wu | 94f48e5 | 2018-07-25 15:28:31 +0800 | [diff] [blame] | 289 | opts.session, opts.chromeos_root, opts.chromeos_repo_mirror_dir, |
| 290 | opts.android_root, opts.android_repo_mirror_dir, opts.chrome_root, |
| 291 | opts.gclient_cache_dir, opts.board, opts.noisy, opts.dut) |
Kuang-che Wu | 1fcc022 | 2018-07-07 16:43:22 +0800 | [diff] [blame] | 292 | |
| 293 | eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall'] |
| 294 | # Do not specify version for autotest prebuilt switching here. The trick is |
| 295 | # that version number is obtained via bisector's environment variable |
| 296 | # CROS_VERSION. |
| 297 | extra_switch_cmd = common_switch_cmd |
| 298 | if not diagnoser.narrow_down_chromeos_prebuilt( |
| 299 | opts.old, opts.new, eval_cmd, extra_switch_cmd=extra_switch_cmd): |
| 300 | return |
| 301 | |
| 302 | diagnoser.switch_chromeos_to_old(force=opts.always_reflash) |
| 303 | util.check_call(*(common_switch_cmd + [diagnoser.cros_old])) |
| 304 | util.check_call('ssh', opts.dut, 'rm', '-rf', '/usr/local/autotest') |
| 305 | |
| 306 | if diagnoser.narrow_down_android(eval_cmd) is not None: |
| 307 | return |
| 308 | # Assume it's ok to leave random version of android prebuilt on DUT. |
| 309 | |
| 310 | # Don't --reinstall to keep chrome binaries override. |
| 311 | eval_cmd = common_eval_cmd + ['--prebuilt'] |
| 312 | if diagnoser.narrow_down_chrome( |
| 313 | eval_cmd, chrome_binaries=chrome_binaries) is not None: |
| 314 | return |
| 315 | |
| 316 | eval_cmd = common_eval_cmd + ['--reinstall'] |
| 317 | if diagnoser.narrow_down_chromeos_localbuild(eval_cmd): |
| 318 | logger.info('%s done', __file__) |
| 319 | |
| 320 | |
| 321 | if __name__ == '__main__': |
| 322 | main() |