blob: 5cd8960ee0883123c0e21c4db2d476399638e249 [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
25from bisect_kit import core
26from bisect_kit import cros_lab_util
27from bisect_kit import cros_util
28from bisect_kit import diagnoser_cros
29from 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 +080058class DiagnoseStates(core.States):
59 """Diagnose states."""
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080060
Kuang-che Wua41525a2018-10-17 23:52:24 +080061 def init(self, config):
62 self.set_data(dict(config=config))
Kuang-che Wud8fc9572018-10-03 21:00:41 +080063
Kuang-che Wua41525a2018-10-17 23:52:24 +080064 @property
65 def config(self):
66 return self.data['config']
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080067
68
Kuang-che Wua41525a2018-10-17 23:52:24 +080069def grab_dut(config):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080070 # Assume "DEPENDENCIES" is identical between the period of
71 # `old` and `new` version.
Kuang-che Wua41525a2018-10-17 23:52:24 +080072 autotest_dir = os.path.join(config['chromeos_root'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080073 cros_util.prebuilt_autotest_dir)
Kuang-che Wua41525a2018-10-17 23:52:24 +080074 info = cros_util.get_autotest_test_info(autotest_dir, config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080075 assert info
76
77 extra_labels = []
78 dependencies = info.variables.get('DEPENDENCIES', '')
79 for label in dependencies.split(','):
80 label = label.strip()
81 # Skip non-machine labels
82 if label in ['cleanup-reboot']:
83 continue
84 extra_labels.append(label)
85
Kuang-che Wua41525a2018-10-17 23:52:24 +080086 reason = 'bisect-kit: %s' % config['session']
Kuang-che Wu248c5182018-10-19 17:08:11 +080087 host = cros_lab_util.allocate_host(reason, config['model'], config['sku'],
Kuang-che Wua41525a2018-10-17 23:52:24 +080088 extra_labels)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080089 if not host:
90 logger.error('unable to allocate dut')
91 return None
92
93 logger.info('allocated host %s', host)
94 return host
95
96
97def may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
98 info = cros_util.get_autotest_test_info(autotest_dir, test_name)
99 assert info
100 dirpath = os.path.dirname(info.path)
101 for pypath in glob.glob(os.path.join(dirpath, '*.py')):
102 if 'ChromeBinaryTest' in open(pypath).read():
103 return True
104 return False
105
106
Kuang-che Wua41525a2018-10-17 23:52:24 +0800107def determine_chrome_binaries(chromeos_root, test_name):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800108 chrome_binaries = None
109 for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items():
Kuang-che Wua41525a2018-10-17 23:52:24 +0800110 if fnmatch.fnmatch(test_name, name_pattern):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800111 chrome_binaries = binaries
112 break
113
Kuang-che Wua41525a2018-10-17 23:52:24 +0800114 autotest_dir = os.path.join(chromeos_root, cros_util.prebuilt_autotest_dir)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800115 if chrome_binaries:
116 logger.info('This test depends on chrome binary: %s', chrome_binaries)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800117 elif may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
Kuang-che Wu74768d32018-09-07 12:03:24 +0800118 logger.warning(
119 '%s code used ChromeBinaryTest but the binary is unknown; '
Kuang-che Wua41525a2018-10-17 23:52:24 +0800120 'please update CHROME_BINARIES_OF_TEST table', test_name)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800121 return chrome_binaries
122
123
Kuang-che Wua41525a2018-10-17 23:52:24 +0800124class DiagnoseCommandLine(object):
125 """Diagnose command line interface."""
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800126
Kuang-che Wua41525a2018-10-17 23:52:24 +0800127 def __init__(self):
128 common.init()
129 self.argument_parser = self.create_argument_parser()
130 self.states = None
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800131
Kuang-che Wua41525a2018-10-17 23:52:24 +0800132 @property
133 def config(self):
134 return self.states.config
135
136 def check_options(self, opts, path_factory):
137 if not opts.chromeos_mirror:
138 opts.chromeos_mirror = path_factory.get_chromeos_mirror()
139 logger.info('chromeos_mirror = %s', opts.chromeos_mirror)
140 if not opts.chromeos_root:
141 opts.chromeos_root = path_factory.get_chromeos_tree()
142 logger.info('chromeos_root = %s', opts.chromeos_root)
143 if not opts.chrome_mirror:
144 opts.chrome_mirror = path_factory.get_chrome_cache()
145 logger.info('chrome_mirror = %s', opts.chrome_mirror)
146 if not opts.chrome_root:
147 opts.chrome_root = path_factory.get_chrome_tree()
148 logger.info('chrome_root = %s', opts.chrome_root)
149
Kuang-che Wu248c5182018-10-19 17:08:11 +0800150 if opts.dut == cros_lab_util.LAB_DUT:
151 if not opts.model and not opts.sku:
152 self.argument_parser.error(
153 'either --model or --sku need to be specified if DUT is "%s"' %
154 cros_lab_util.LAB_DUT)
155 # Board name cannot be deduced from auto allocated devices because they
156 # may be provisioned with image of unexpected board.
157 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800158 self.argument_parser.error('--board need to be specified if DUT is "%s"'
159 % cros_lab_util.LAB_DUT)
Kuang-che Wu248c5182018-10-19 17:08:11 +0800160 else:
161 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800162 opts.board = cros_util.query_dut_board(opts.dut)
163
164 if cros_util.is_cros_short_version(opts.old):
165 opts.old = cros_util.version_to_full(opts.board, opts.old)
166 if cros_util.is_cros_short_version(opts.new):
167 opts.new = cros_util.version_to_full(opts.board, opts.new)
168
169 if opts.metric:
170 if opts.old_value is None:
171 self.argument_parser.error('--old_value is not provided')
172 if opts.new_value is None:
173 self.argument_parser.error('--new_value is not provided')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800174 else:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800175 if opts.old_value is not None:
176 self.argument_parser.error(
177 '--old_value is provided but --metric is not')
178 if opts.new_value is not None:
179 self.argument_parser.error(
180 '--new_value is provided but --metric is not')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800181
Kuang-che Wua41525a2018-10-17 23:52:24 +0800182 def cmd_init(self, opts):
183 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
184 opts.mirror_base, opts.work_base, opts.session)
185 self.check_options(opts, path_factory)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800186
Kuang-che Wua41525a2018-10-17 23:52:24 +0800187 config = dict(
188 session=opts.session,
189 mirror_base=opts.mirror_base,
190 work_base=opts.work_base,
191 chromeos_root=opts.chromeos_root,
192 chromeos_mirror=opts.chromeos_mirror,
193 chrome_root=opts.chrome_root,
194 chrome_mirror=opts.chrome_mirror,
195 android_root=opts.android_root,
196 android_mirror=opts.android_mirror,
Kuang-che Wu248c5182018-10-19 17:08:11 +0800197 dut=opts.dut,
198 model=opts.model,
199 sku=opts.sku,
200 board=opts.board,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800201 old=opts.old,
202 new=opts.new,
203 test_name=opts.test_name,
204 metric=opts.metric,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800205 old_value=opts.old_value,
206 new_value=opts.new_value,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800207 noisy=opts.noisy,
208 test_that_args=opts.args,
209 always_reflash=opts.always_reflash,
210 )
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800211
Kuang-che Wua41525a2018-10-17 23:52:24 +0800212 self.states.init(config)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800213
Kuang-che Wua41525a2018-10-17 23:52:24 +0800214 # Unpack old autotest prebuilt, assume following information don't change
215 # between versions:
216 # - what chrome binaries to run
217 # - dependency labels for DUT allocation
218 common_switch_cmd, _common_eval_cmd = self._build_cmds()
219 util.check_call(*(common_switch_cmd + [self.config['old']]))
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800220
Kuang-che Wua41525a2018-10-17 23:52:24 +0800221 self.states.save()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800222
Kuang-che Wua41525a2018-10-17 23:52:24 +0800223 def _build_cmds(self):
224 # prebuilt version will be specified later.
225 common_switch_cmd = [
226 './switch_autotest_prebuilt.py',
227 '--chromeos_root', self.config['chromeos_root'],
228 '--test_name', self.config['test_name'],
229 '--board', self.config['board'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800230 ] # yapf: disable
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800231
Kuang-che Wua41525a2018-10-17 23:52:24 +0800232 common_eval_cmd = [
233 './eval_cros_autotest.py',
234 '--chromeos_root', self.config['chromeos_root'],
235 '--test_name', self.config['test_name'],
236 ] # yapf: disable
237 if self.config['metric']:
238 common_eval_cmd += [
239 '--metric', self.config['metric'],
240 '--old_value', str(self.config['old_value']),
241 '--new_value', str(self.config['new_value']),
242 ] # yapf: disable
243 if self.config['test_that_args']:
244 common_eval_cmd += ['--args', self.config['test_that_args']]
245 return common_switch_cmd, common_eval_cmd
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800246
Kuang-che Wua41525a2018-10-17 23:52:24 +0800247 def cmd_run(self, opts):
248 del opts # unused
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800249
Kuang-che Wua41525a2018-10-17 23:52:24 +0800250 self.states.load()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800251
Kuang-che Wua41525a2018-10-17 23:52:24 +0800252 if (self.config['dut'] == cros_lab_util.LAB_DUT and
253 self.config.get('allocated_dut')):
254 self.argument_parser.error(
255 'do not resume if the dut is dynamically allocated')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800256
Kuang-che Wua41525a2018-10-17 23:52:24 +0800257 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
258 self.config['mirror_base'], self.config['work_base'],
259 self.config['session'])
260 common_switch_cmd, common_eval_cmd = self._build_cmds()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800261
Kuang-che Wua41525a2018-10-17 23:52:24 +0800262 chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
263 self.config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800264
Kuang-che Wua41525a2018-10-17 23:52:24 +0800265 with cros_lab_util.dut_manager(self.config['dut'],
266 lambda: grab_dut(self.config)) as dut:
267 if not dut:
268 raise core.ExecutionFatalError('unable to allocate DUT')
269 assert cros_util.is_dut(dut)
270 self.config['allocated_dut'] = dut
271 self.states.save()
272 common_eval_cmd.append(dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800273
Kuang-che Wua41525a2018-10-17 23:52:24 +0800274 diagnoser = diagnoser_cros.CrosDiagnoser(
275 self.config['session'], path_factory, self.config['chromeos_root'],
276 self.config['chromeos_mirror'], self.config['android_root'],
277 self.config['android_mirror'], self.config['chrome_root'],
278 self.config['chrome_mirror'], self.config['board'],
279 self.config['noisy'], dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800280
Kuang-che Wua41525a2018-10-17 23:52:24 +0800281 eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
282 # Do not specify version for autotest prebuilt switching here. The trick
283 # is that version number is obtained via bisector's environment variable
284 # CROS_VERSION.
285 extra_switch_cmd = common_switch_cmd
286 if not diagnoser.narrow_down_chromeos_prebuilt(
287 self.config['old'],
288 self.config['new'],
289 eval_cmd,
290 extra_switch_cmd=extra_switch_cmd):
291 return
292
293 diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
294 util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
295 util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
296
297 if diagnoser.narrow_down_android(eval_cmd) is not None:
298 return
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 if diagnoser.narrow_down_chrome(
304 eval_cmd, chrome_binaries=chrome_binaries) is not None:
305 return
306
307 eval_cmd = common_eval_cmd + ['--reinstall']
308 if diagnoser.narrow_down_chromeos_localbuild(eval_cmd):
309 logger.info('%s done', __file__)
310
311 def create_argument_parser(self):
312 parser = argparse.ArgumentParser()
313 common.add_common_arguments(parser)
314 parser.add_argument('--session_base', default='bisect.sessions')
315 parser.add_argument('--session', help='Session name', required=True)
316 subparsers = parser.add_subparsers(
317 dest='command', title='commands', metavar='<command>')
318
319 parser_init = subparsers.add_parser('init', help='Initialize')
320 group = parser_init.add_argument_group(
321 title='Source tree path options',
322 description='''
323 Specify the paths of chromeos/chrome/android mirror and checkout. They
324 have the same default values as setup_cros_bisect.py, so usually you can
325 omit them and it just works.
326 ''')
327 group.add_argument(
328 '--mirror_base',
329 metavar='MIRROR_BASE',
330 default=configure.get('MIRROR_BASE',
331 setup_cros_bisect.DEFAULT_MIRROR_BASE),
332 help='Directory for mirrors (default: %(default)s)')
333 group.add_argument(
334 '--work_base',
335 metavar='WORK_BASE',
336 default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
337 help='Directory for bisection working directories '
338 '(default: %(default)s)')
339 group.add_argument(
340 '--chromeos_root',
341 metavar='CHROMEOS_ROOT',
342 type=cli.argtype_dir_path,
343 default=configure.get('CHROMEOS_ROOT'),
344 help='ChromeOS tree root')
345 group.add_argument(
346 '--chromeos_mirror',
347 type=cli.argtype_dir_path,
348 default=configure.get('CHROMEOS_MIRROR'),
349 help='ChromeOS repo mirror path')
350 group.add_argument(
351 '--android_root',
352 metavar='ANDROID_ROOT',
353 type=cli.argtype_dir_path,
354 default=configure.get('ANDROID_ROOT'),
355 help='Android tree root')
356 group.add_argument(
357 '--android_mirror',
358 type=cli.argtype_dir_path,
359 default=configure.get('ANDROID_MIRROR'),
360 help='Android repo mirror path')
361 group.add_argument(
362 '--chrome_root',
363 metavar='CHROME_ROOT',
364 type=cli.argtype_dir_path,
365 default=configure.get('CHROME_ROOT'),
366 help='Chrome tree root')
367 group.add_argument(
368 '--chrome_mirror',
369 metavar='CHROME_MIRROR',
370 type=cli.argtype_dir_path,
371 default=configure.get('CHROME_MIRROR'),
372 help="chrome's gclient cache dir")
373
Kuang-che Wu248c5182018-10-19 17:08:11 +0800374 group = parser_init.add_argument_group(title='DUT allocation options')
375 group.add_argument(
376 '--dut',
377 metavar='DUT',
378 required=True,
379 help='Address of DUT (Device Under Test). If "%s", DUT will be '
380 'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
381 group.add_argument(
382 '--model',
383 metavar='MODEL',
384 help='"model" criteria if DUT is auto allocated from the lab')
385 group.add_argument(
386 '--sku',
387 metavar='SKU',
388 help='"sku" criteria if DUT is auto allocated from the lab')
389
Kuang-che Wua41525a2018-10-17 23:52:24 +0800390 group = parser_init.add_argument_group(title='Essential options')
391 group.add_argument(
Kuang-che Wu248c5182018-10-19 17:08:11 +0800392 '--board',
393 metavar='BOARD',
394 default=configure.get('BOARD'),
395 help='ChromeOS board name; auto detected if DUT is not auto allocated')
396 group.add_argument(
Kuang-che Wua41525a2018-10-17 23:52:24 +0800397 '--old',
398 type=cros_util.argtype_cros_version,
399 required=True,
400 help='ChromeOS version with old behavior')
401 group.add_argument(
402 '--new',
403 type=cros_util.argtype_cros_version,
404 required=True,
405 help='ChromeOS version with new behavior')
406 group.add_argument('--test_name', required=True, help='Test name')
407
408 group = parser_init.add_argument_group(title='Options for benchmark test')
409 group.add_argument('--metric', help='Metric name of benchmark test')
410 group.add_argument(
411 '--old_value',
412 type=float,
413 help='For benchmark test, old value of metric')
414 group.add_argument(
415 '--new_value',
416 type=float,
417 help='For benchmark test, new value of metric')
418
Kuang-che Wua41525a2018-10-17 23:52:24 +0800419 group = parser_init.add_argument_group(title='Options passed to test_that')
420 group.add_argument(
421 '--args',
422 help='Extra args passed to "test_that --args"; Overrides the default')
423
424 group = parser_init.add_argument_group(title='Bisect behavior options')
425 group.add_argument(
426 '--noisy',
427 help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
428 group.add_argument(
429 '--always_reflash',
430 action='store_true',
431 help='Do not trust ChromeOS version number of DUT and always reflash. '
432 'This is usually only needed when resume because previous bisect was '
433 'interrupted and the DUT may be in an unexpected state')
434 parser_init.set_defaults(func=self.cmd_init)
435
436 parser_run = subparsers.add_parser('run', help='Start auto bisection')
437 parser_run.set_defaults(func=self.cmd_run)
438
439 return parser
440
441 def main(self, args=None):
442 opts = self.argument_parser.parse_args(args)
443 common.config_logging(opts)
444
445 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
446 session_file = os.path.join(session_base, opts.session,
447 self.__class__.__name__)
448 self.states = DiagnoseStates(session_file)
449 opts.func(opts)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800450
451
452if __name__ == '__main__':
Kuang-che Wua41525a2018-10-17 23:52:24 +0800453 DiagnoseCommandLine().main()