blob: c10d1b6800f7dc00843f6a8503c130f4f958da00 [file] [log] [blame]
Kuang-che Wu1fcc0222018-07-07 16:43:22 +08001#!/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
8This is integrated bisection utility. Given ChromeOS, Chrome, Android source
Kuang-che Wu927231f2018-07-24 14:21:56 +08009tree, and necessary parameters, this script can determine which components to
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080010bisect, and hopefully output the culprit CL of regression.
11
12Sometimes the script failed to figure out the final CL for various reasons, it
13will cut down the search range as narrow as it can.
14"""
15from __future__ import print_function
16import argparse
17import fnmatch
18import glob
19import logging
20import os
21
22from bisect_kit import cli
23from bisect_kit import common
24from bisect_kit import configure
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080025from bisect_kit import cros_lab_util
26from bisect_kit import cros_util
27from bisect_kit import diagnoser_cros
Kuang-che Wue121fae2018-11-09 16:18:39 +080028from bisect_kit import errors
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080029from bisect_kit import util
Kuang-che Wud8fc9572018-10-03 21:00:41 +080030import setup_cros_bisect
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080031
32logger = logging.getLogger(__name__)
33
34# What chrome binaries to build for given autotest.
35# This dict is created manually by inspecting output of
36# 'grep -r ChromeBinaryTest autotest/files/client/site_tests'
37# If you change this dict, build_and_deploy_chrome_helper.sh may need update
38# as well.
39CHROME_BINARIES_OF_TEST = {
40 'graphics_Chrome.ozone_gl_unittests': ['ozone_gl_unittests'],
41 'security_SandboxLinuxUnittests': ['sandbox_linux_unittests'],
42 'video_HangoutHardwarePerf*': [
43 'video_decode_accelerator_unittest',
44 'video_encode_accelerator_unittest',
45 ],
46 'video_JDAPerf*': ['jpeg_decode_accelerator_unittest'],
47 'video_JEAPerf': ['jpeg_encode_accelerator_unittest'],
48 'video_JpegDecodeAccelerator': ['jpeg_decode_accelerator_unittest'],
49 'video_JpegEncodeAccelerator': ['jpeg_encode_accelerator_unittest'],
50 'video_VDAPerf': ['video_decode_accelerator_unittest'],
51 'video_VDASanity': ['video_decode_accelerator_unittest'],
52 'video_VEAPerf': ['video_encode_accelerator_unittest'],
53 'video_VideoDecodeAccelerator*': ['video_decode_accelerator_unittest'],
54 'video_VideoEncodeAccelerator*': ['video_encode_accelerator_unittest'],
55}
56
57
Kuang-che Wua41525a2018-10-17 23:52:24 +080058def grab_dut(config):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080059 # Assume "DEPENDENCIES" is identical between the period of
60 # `old` and `new` version.
Kuang-che Wua41525a2018-10-17 23:52:24 +080061 autotest_dir = os.path.join(config['chromeos_root'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080062 cros_util.prebuilt_autotest_dir)
Kuang-che Wua41525a2018-10-17 23:52:24 +080063 info = cros_util.get_autotest_test_info(autotest_dir, config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080064 assert info
65
Kuang-che Wua41525a2018-10-17 23:52:24 +080066 reason = 'bisect-kit: %s' % config['session']
Kuang-che Wuf65c61d2018-10-19 17:48:30 +080067 if config.get('allocated_dut'):
68 host_name = cros_lab_util.dut_host_name(config['allocated_dut'])
69 logger.info('try to allocate the same host (%s) as last run', host_name)
70 host = cros_lab_util.allocate_host(reason, host=host_name)
71 else:
72 extra_labels = []
73 dependencies = info.variables.get('DEPENDENCIES', '')
74 for label in dependencies.split(','):
75 label = label.strip()
76 # Skip non-machine labels
77 if label in ['cleanup-reboot']:
78 continue
79 extra_labels.append(label)
80
81 host = cros_lab_util.allocate_host(
82 reason,
83 model=config['model'],
84 sku=config['sku'],
85 extra_labels=extra_labels)
86
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080087 if not host:
88 logger.error('unable to allocate dut')
89 return None
90
91 logger.info('allocated host %s', host)
92 return host
93
94
95def may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
96 info = cros_util.get_autotest_test_info(autotest_dir, test_name)
97 assert info
98 dirpath = os.path.dirname(info.path)
99 for pypath in glob.glob(os.path.join(dirpath, '*.py')):
100 if 'ChromeBinaryTest' in open(pypath).read():
101 return True
102 return False
103
104
Kuang-che Wua41525a2018-10-17 23:52:24 +0800105def determine_chrome_binaries(chromeos_root, test_name):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800106 chrome_binaries = None
107 for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items():
Kuang-che Wua41525a2018-10-17 23:52:24 +0800108 if fnmatch.fnmatch(test_name, name_pattern):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800109 chrome_binaries = binaries
110 break
111
Kuang-che Wua41525a2018-10-17 23:52:24 +0800112 autotest_dir = os.path.join(chromeos_root, cros_util.prebuilt_autotest_dir)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800113 if chrome_binaries:
114 logger.info('This test depends on chrome binary: %s', chrome_binaries)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800115 elif may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
Kuang-che Wu74768d32018-09-07 12:03:24 +0800116 logger.warning(
117 '%s code used ChromeBinaryTest but the binary is unknown; '
Kuang-che Wua41525a2018-10-17 23:52:24 +0800118 'please update CHROME_BINARIES_OF_TEST table', test_name)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800119 return chrome_binaries
120
121
Kuang-che Wua41525a2018-10-17 23:52:24 +0800122class DiagnoseCommandLine(object):
123 """Diagnose command line interface."""
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800124
Kuang-che Wua41525a2018-10-17 23:52:24 +0800125 def __init__(self):
126 common.init()
127 self.argument_parser = self.create_argument_parser()
128 self.states = None
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800129
Kuang-che Wua41525a2018-10-17 23:52:24 +0800130 @property
131 def config(self):
132 return self.states.config
133
134 def check_options(self, opts, path_factory):
135 if not opts.chromeos_mirror:
136 opts.chromeos_mirror = path_factory.get_chromeos_mirror()
137 logger.info('chromeos_mirror = %s', opts.chromeos_mirror)
138 if not opts.chromeos_root:
139 opts.chromeos_root = path_factory.get_chromeos_tree()
140 logger.info('chromeos_root = %s', opts.chromeos_root)
141 if not opts.chrome_mirror:
142 opts.chrome_mirror = path_factory.get_chrome_cache()
143 logger.info('chrome_mirror = %s', opts.chrome_mirror)
144 if not opts.chrome_root:
145 opts.chrome_root = path_factory.get_chrome_tree()
146 logger.info('chrome_root = %s', opts.chrome_root)
147
Kuang-che Wu248c5182018-10-19 17:08:11 +0800148 if opts.dut == cros_lab_util.LAB_DUT:
149 if not opts.model and not opts.sku:
150 self.argument_parser.error(
151 'either --model or --sku need to be specified if DUT is "%s"' %
152 cros_lab_util.LAB_DUT)
153 # Board name cannot be deduced from auto allocated devices because they
154 # may be provisioned with image of unexpected board.
155 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800156 self.argument_parser.error('--board need to be specified if DUT is "%s"'
157 % cros_lab_util.LAB_DUT)
Kuang-che Wu248c5182018-10-19 17:08:11 +0800158 else:
159 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800160 opts.board = cros_util.query_dut_board(opts.dut)
161
162 if cros_util.is_cros_short_version(opts.old):
163 opts.old = cros_util.version_to_full(opts.board, opts.old)
164 if cros_util.is_cros_short_version(opts.new):
165 opts.new = cros_util.version_to_full(opts.board, opts.new)
166
167 if opts.metric:
168 if opts.old_value is None:
169 self.argument_parser.error('--old_value is not provided')
170 if opts.new_value is None:
171 self.argument_parser.error('--new_value is not provided')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800172 else:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800173 if opts.old_value is not None:
174 self.argument_parser.error(
175 '--old_value is provided but --metric is not')
176 if opts.new_value is not None:
177 self.argument_parser.error(
178 '--new_value is provided but --metric is not')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800179
Kuang-che Wua41525a2018-10-17 23:52:24 +0800180 def cmd_init(self, opts):
181 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
182 opts.mirror_base, opts.work_base, opts.session)
183 self.check_options(opts, path_factory)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800184
Kuang-che Wua41525a2018-10-17 23:52:24 +0800185 config = dict(
186 session=opts.session,
187 mirror_base=opts.mirror_base,
188 work_base=opts.work_base,
189 chromeos_root=opts.chromeos_root,
190 chromeos_mirror=opts.chromeos_mirror,
191 chrome_root=opts.chrome_root,
192 chrome_mirror=opts.chrome_mirror,
193 android_root=opts.android_root,
194 android_mirror=opts.android_mirror,
Kuang-che Wu248c5182018-10-19 17:08:11 +0800195 dut=opts.dut,
196 model=opts.model,
197 sku=opts.sku,
198 board=opts.board,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800199 old=opts.old,
200 new=opts.new,
201 test_name=opts.test_name,
202 metric=opts.metric,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800203 old_value=opts.old_value,
204 new_value=opts.new_value,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800205 noisy=opts.noisy,
206 test_that_args=opts.args,
207 always_reflash=opts.always_reflash,
208 )
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800209
Kuang-che Wua41525a2018-10-17 23:52:24 +0800210 self.states.init(config)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800211
Kuang-che Wua41525a2018-10-17 23:52:24 +0800212 # Unpack old autotest prebuilt, assume following information don't change
213 # between versions:
214 # - what chrome binaries to run
215 # - dependency labels for DUT allocation
216 common_switch_cmd, _common_eval_cmd = self._build_cmds()
217 util.check_call(*(common_switch_cmd + [self.config['old']]))
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800218
Kuang-che Wua41525a2018-10-17 23:52:24 +0800219 self.states.save()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800220
Kuang-che Wua41525a2018-10-17 23:52:24 +0800221 def _build_cmds(self):
222 # prebuilt version will be specified later.
223 common_switch_cmd = [
224 './switch_autotest_prebuilt.py',
225 '--chromeos_root', self.config['chromeos_root'],
226 '--test_name', self.config['test_name'],
227 '--board', self.config['board'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800228 ] # yapf: disable
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800229
Kuang-che Wua41525a2018-10-17 23:52:24 +0800230 common_eval_cmd = [
231 './eval_cros_autotest.py',
232 '--chromeos_root', self.config['chromeos_root'],
233 '--test_name', self.config['test_name'],
234 ] # yapf: disable
235 if self.config['metric']:
236 common_eval_cmd += [
237 '--metric', self.config['metric'],
238 '--old_value', str(self.config['old_value']),
239 '--new_value', str(self.config['new_value']),
240 ] # yapf: disable
241 if self.config['test_that_args']:
242 common_eval_cmd += ['--args', self.config['test_that_args']]
243 return common_switch_cmd, common_eval_cmd
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800244
Kuang-che Wua41525a2018-10-17 23:52:24 +0800245 def cmd_run(self, opts):
246 del opts # unused
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800247
Kuang-che Wua41525a2018-10-17 23:52:24 +0800248 self.states.load()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800249
Kuang-che Wu8b654092018-11-09 17:56:25 +0800250 try:
251 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
252 self.config['mirror_base'], self.config['work_base'],
253 self.config['session'])
254 common_switch_cmd, common_eval_cmd = self._build_cmds()
255
256 chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
257 self.config['test_name'])
258
259 with cros_lab_util.dut_manager(self.config['dut'],
260 lambda: grab_dut(self.config)) as dut:
261 if not dut:
262 raise errors.NoDutAvailable('unable to allocate DUT')
263 assert cros_util.is_dut(dut)
264 if self.config['dut'] == cros_lab_util.LAB_DUT:
265 self.config['allocated_dut'] = dut
266 self.states.save()
267 common_eval_cmd.append(dut)
268
269 diagnoser = diagnoser_cros.CrosDiagnoser(
270 self.states, self.config['session'], path_factory,
271 self.config['chromeos_root'], self.config['chromeos_mirror'],
272 self.config['android_root'], self.config['android_mirror'],
273 self.config['chrome_root'], self.config['chrome_mirror'],
274 self.config['board'], self.config['noisy'], dut)
275
276 eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
277 # Do not specify version for autotest prebuilt switching here. The trick
278 # is that version number is obtained via bisector's environment variable
279 # CROS_VERSION.
280 extra_switch_cmd = common_switch_cmd
281 diagnoser.narrow_down_chromeos_prebuilt(
282 self.config['old'],
283 self.config['new'],
284 eval_cmd,
285 extra_switch_cmd=extra_switch_cmd)
286
287 diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
288 util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
289 util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
290
291 try:
292 if diagnoser.narrow_down_android(eval_cmd):
293 return
294 except errors.VerificationFailed:
295 raise
296 except Exception:
297 logger.exception('exception in android bisector before verification; '
298 'assume culprit is not inside android and continue')
299 # Assume it's ok to leave random version of android prebuilt on DUT.
300
301 # Don't --reinstall to keep chrome binaries override.
302 eval_cmd = common_eval_cmd + ['--prebuilt']
303 try:
304 if diagnoser.narrow_down_chrome(
305 eval_cmd, chrome_binaries=chrome_binaries) is not None:
306 return
307 except errors.VerifyOldFailed:
308 logger.fatal('expect old chrome has old behavior but failed')
309 raise
310 except Exception:
311 logger.exception('exception in chrome bisector before verification; '
312 'assume culprit is not inside chrome and continue')
313
314 eval_cmd = common_eval_cmd + ['--reinstall']
315 diagnoser.narrow_down_chromeos_localbuild(eval_cmd)
316 logger.info('%s done', __file__)
317 except Exception as e:
318 logger.exception('got exception; stop')
319 exception_name = e.__class__.__name__
320 self.states.add_history(
321 'failed', '%s: %s' % (exception_name, e), exception=exception_name)
322
323 def cmd_log(self, opts):
324 self.states.load()
Kuang-che Wua41525a2018-10-17 23:52:24 +0800325 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
326 self.config['mirror_base'], self.config['work_base'],
327 self.config['session'])
Kuang-che Wu8b654092018-11-09 17:56:25 +0800328 diagnoser = diagnoser_cros.CrosDiagnoser(
329 self.states, self.config['session'], path_factory,
330 self.config['chromeos_root'], self.config['chromeos_mirror'],
331 self.config['android_root'], self.config['android_mirror'],
332 self.config['chrome_root'], self.config['chrome_mirror'],
333 self.config['board'], self.config['noisy'], None)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800334
Kuang-che Wu8b654092018-11-09 17:56:25 +0800335 diagnoser.cmd_log(opts.json)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800336
337 def create_argument_parser(self):
338 parser = argparse.ArgumentParser()
339 common.add_common_arguments(parser)
340 parser.add_argument('--session_base', default='bisect.sessions')
341 parser.add_argument('--session', help='Session name', required=True)
342 subparsers = parser.add_subparsers(
343 dest='command', title='commands', metavar='<command>')
344
345 parser_init = subparsers.add_parser('init', help='Initialize')
346 group = parser_init.add_argument_group(
347 title='Source tree path options',
348 description='''
349 Specify the paths of chromeos/chrome/android mirror and checkout. They
350 have the same default values as setup_cros_bisect.py, so usually you can
351 omit them and it just works.
352 ''')
353 group.add_argument(
354 '--mirror_base',
355 metavar='MIRROR_BASE',
356 default=configure.get('MIRROR_BASE',
357 setup_cros_bisect.DEFAULT_MIRROR_BASE),
358 help='Directory for mirrors (default: %(default)s)')
359 group.add_argument(
360 '--work_base',
361 metavar='WORK_BASE',
362 default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
363 help='Directory for bisection working directories '
364 '(default: %(default)s)')
365 group.add_argument(
366 '--chromeos_root',
367 metavar='CHROMEOS_ROOT',
368 type=cli.argtype_dir_path,
369 default=configure.get('CHROMEOS_ROOT'),
370 help='ChromeOS tree root')
371 group.add_argument(
372 '--chromeos_mirror',
373 type=cli.argtype_dir_path,
374 default=configure.get('CHROMEOS_MIRROR'),
375 help='ChromeOS repo mirror path')
376 group.add_argument(
377 '--android_root',
378 metavar='ANDROID_ROOT',
379 type=cli.argtype_dir_path,
380 default=configure.get('ANDROID_ROOT'),
381 help='Android tree root')
382 group.add_argument(
383 '--android_mirror',
384 type=cli.argtype_dir_path,
385 default=configure.get('ANDROID_MIRROR'),
386 help='Android repo mirror path')
387 group.add_argument(
388 '--chrome_root',
389 metavar='CHROME_ROOT',
390 type=cli.argtype_dir_path,
391 default=configure.get('CHROME_ROOT'),
392 help='Chrome tree root')
393 group.add_argument(
394 '--chrome_mirror',
395 metavar='CHROME_MIRROR',
396 type=cli.argtype_dir_path,
397 default=configure.get('CHROME_MIRROR'),
398 help="chrome's gclient cache dir")
399
Kuang-che Wu248c5182018-10-19 17:08:11 +0800400 group = parser_init.add_argument_group(title='DUT allocation options')
401 group.add_argument(
402 '--dut',
403 metavar='DUT',
404 required=True,
405 help='Address of DUT (Device Under Test). If "%s", DUT will be '
406 'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
407 group.add_argument(
408 '--model',
409 metavar='MODEL',
410 help='"model" criteria if DUT is auto allocated from the lab')
411 group.add_argument(
412 '--sku',
413 metavar='SKU',
414 help='"sku" criteria if DUT is auto allocated from the lab')
415
Kuang-che Wua41525a2018-10-17 23:52:24 +0800416 group = parser_init.add_argument_group(title='Essential options')
417 group.add_argument(
Kuang-che Wu248c5182018-10-19 17:08:11 +0800418 '--board',
419 metavar='BOARD',
420 default=configure.get('BOARD'),
421 help='ChromeOS board name; auto detected if DUT is not auto allocated')
422 group.add_argument(
Kuang-che Wua41525a2018-10-17 23:52:24 +0800423 '--old',
424 type=cros_util.argtype_cros_version,
425 required=True,
426 help='ChromeOS version with old behavior')
427 group.add_argument(
428 '--new',
429 type=cros_util.argtype_cros_version,
430 required=True,
431 help='ChromeOS version with new behavior')
432 group.add_argument('--test_name', required=True, help='Test name')
433
434 group = parser_init.add_argument_group(title='Options for benchmark test')
435 group.add_argument('--metric', help='Metric name of benchmark test')
436 group.add_argument(
437 '--old_value',
438 type=float,
439 help='For benchmark test, old value of metric')
440 group.add_argument(
441 '--new_value',
442 type=float,
443 help='For benchmark test, new value of metric')
444
Kuang-che Wua41525a2018-10-17 23:52:24 +0800445 group = parser_init.add_argument_group(title='Options passed to test_that')
446 group.add_argument(
447 '--args',
448 help='Extra args passed to "test_that --args"; Overrides the default')
449
450 group = parser_init.add_argument_group(title='Bisect behavior options')
451 group.add_argument(
452 '--noisy',
453 help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
454 group.add_argument(
455 '--always_reflash',
456 action='store_true',
457 help='Do not trust ChromeOS version number of DUT and always reflash. '
458 'This is usually only needed when resume because previous bisect was '
459 'interrupted and the DUT may be in an unexpected state')
460 parser_init.set_defaults(func=self.cmd_init)
461
462 parser_run = subparsers.add_parser('run', help='Start auto bisection')
463 parser_run.set_defaults(func=self.cmd_run)
464
Kuang-che Wu8b654092018-11-09 17:56:25 +0800465 parser_log = subparsers.add_parser(
466 'log', help='Prints what has been done so far')
467 parser_log.add_argument(
468 '--json', action='store_true', help='Machine readable output')
469 parser_log.set_defaults(func=self.cmd_log)
470
Kuang-che Wua41525a2018-10-17 23:52:24 +0800471 return parser
472
473 def main(self, args=None):
474 opts = self.argument_parser.parse_args(args)
475 common.config_logging(opts)
476
477 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
478 session_file = os.path.join(session_base, opts.session,
479 self.__class__.__name__)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800480 self.states = diagnoser_cros.DiagnoseStates(session_file)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800481 opts.func(opts)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800482
483
484if __name__ == '__main__':
Kuang-che Wua41525a2018-10-17 23:52:24 +0800485 DiagnoseCommandLine().main()