blob: ff070b0a7c81e4d4a6fceb73e2af980c128835f1 [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
Kuang-che Wue121fae2018-11-09 16:18:39 +080029from bisect_kit import errors
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080030from bisect_kit import util
Kuang-che Wud8fc9572018-10-03 21:00:41 +080031import setup_cros_bisect
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080032
33logger = logging.getLogger(__name__)
34
35# What chrome binaries to build for given autotest.
36# This dict is created manually by inspecting output of
37# 'grep -r ChromeBinaryTest autotest/files/client/site_tests'
38# If you change this dict, build_and_deploy_chrome_helper.sh may need update
39# as well.
40CHROME_BINARIES_OF_TEST = {
41 'graphics_Chrome.ozone_gl_unittests': ['ozone_gl_unittests'],
42 'security_SandboxLinuxUnittests': ['sandbox_linux_unittests'],
43 'video_HangoutHardwarePerf*': [
44 'video_decode_accelerator_unittest',
45 'video_encode_accelerator_unittest',
46 ],
47 'video_JDAPerf*': ['jpeg_decode_accelerator_unittest'],
48 'video_JEAPerf': ['jpeg_encode_accelerator_unittest'],
49 'video_JpegDecodeAccelerator': ['jpeg_decode_accelerator_unittest'],
50 'video_JpegEncodeAccelerator': ['jpeg_encode_accelerator_unittest'],
51 'video_VDAPerf': ['video_decode_accelerator_unittest'],
52 'video_VDASanity': ['video_decode_accelerator_unittest'],
53 'video_VEAPerf': ['video_encode_accelerator_unittest'],
54 'video_VideoDecodeAccelerator*': ['video_decode_accelerator_unittest'],
55 'video_VideoEncodeAccelerator*': ['video_encode_accelerator_unittest'],
56}
57
58
Kuang-che Wua41525a2018-10-17 23:52:24 +080059class DiagnoseStates(core.States):
60 """Diagnose states."""
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080061
Kuang-che Wua41525a2018-10-17 23:52:24 +080062 def init(self, config):
63 self.set_data(dict(config=config))
Kuang-che Wud8fc9572018-10-03 21:00:41 +080064
Kuang-che Wua41525a2018-10-17 23:52:24 +080065 @property
66 def config(self):
67 return self.data['config']
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080068
69
Kuang-che Wua41525a2018-10-17 23:52:24 +080070def grab_dut(config):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080071 # Assume "DEPENDENCIES" is identical between the period of
72 # `old` and `new` version.
Kuang-che Wua41525a2018-10-17 23:52:24 +080073 autotest_dir = os.path.join(config['chromeos_root'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080074 cros_util.prebuilt_autotest_dir)
Kuang-che Wua41525a2018-10-17 23:52:24 +080075 info = cros_util.get_autotest_test_info(autotest_dir, config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080076 assert info
77
Kuang-che Wua41525a2018-10-17 23:52:24 +080078 reason = 'bisect-kit: %s' % config['session']
Kuang-che Wuf65c61d2018-10-19 17:48:30 +080079 if config.get('allocated_dut'):
80 host_name = cros_lab_util.dut_host_name(config['allocated_dut'])
81 logger.info('try to allocate the same host (%s) as last run', host_name)
82 host = cros_lab_util.allocate_host(reason, host=host_name)
83 else:
84 extra_labels = []
85 dependencies = info.variables.get('DEPENDENCIES', '')
86 for label in dependencies.split(','):
87 label = label.strip()
88 # Skip non-machine labels
89 if label in ['cleanup-reboot']:
90 continue
91 extra_labels.append(label)
92
93 host = cros_lab_util.allocate_host(
94 reason,
95 model=config['model'],
96 sku=config['sku'],
97 extra_labels=extra_labels)
98
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080099 if not host:
100 logger.error('unable to allocate dut')
101 return None
102
103 logger.info('allocated host %s', host)
104 return host
105
106
107def may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
108 info = cros_util.get_autotest_test_info(autotest_dir, test_name)
109 assert info
110 dirpath = os.path.dirname(info.path)
111 for pypath in glob.glob(os.path.join(dirpath, '*.py')):
112 if 'ChromeBinaryTest' in open(pypath).read():
113 return True
114 return False
115
116
Kuang-che Wua41525a2018-10-17 23:52:24 +0800117def determine_chrome_binaries(chromeos_root, test_name):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800118 chrome_binaries = None
119 for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items():
Kuang-che Wua41525a2018-10-17 23:52:24 +0800120 if fnmatch.fnmatch(test_name, name_pattern):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800121 chrome_binaries = binaries
122 break
123
Kuang-che Wua41525a2018-10-17 23:52:24 +0800124 autotest_dir = os.path.join(chromeos_root, cros_util.prebuilt_autotest_dir)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800125 if chrome_binaries:
126 logger.info('This test depends on chrome binary: %s', chrome_binaries)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800127 elif may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
Kuang-che Wu74768d32018-09-07 12:03:24 +0800128 logger.warning(
129 '%s code used ChromeBinaryTest but the binary is unknown; '
Kuang-che Wua41525a2018-10-17 23:52:24 +0800130 'please update CHROME_BINARIES_OF_TEST table', test_name)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800131 return chrome_binaries
132
133
Kuang-che Wua41525a2018-10-17 23:52:24 +0800134class DiagnoseCommandLine(object):
135 """Diagnose command line interface."""
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800136
Kuang-che Wua41525a2018-10-17 23:52:24 +0800137 def __init__(self):
138 common.init()
139 self.argument_parser = self.create_argument_parser()
140 self.states = None
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800141
Kuang-che Wua41525a2018-10-17 23:52:24 +0800142 @property
143 def config(self):
144 return self.states.config
145
146 def check_options(self, opts, path_factory):
147 if not opts.chromeos_mirror:
148 opts.chromeos_mirror = path_factory.get_chromeos_mirror()
149 logger.info('chromeos_mirror = %s', opts.chromeos_mirror)
150 if not opts.chromeos_root:
151 opts.chromeos_root = path_factory.get_chromeos_tree()
152 logger.info('chromeos_root = %s', opts.chromeos_root)
153 if not opts.chrome_mirror:
154 opts.chrome_mirror = path_factory.get_chrome_cache()
155 logger.info('chrome_mirror = %s', opts.chrome_mirror)
156 if not opts.chrome_root:
157 opts.chrome_root = path_factory.get_chrome_tree()
158 logger.info('chrome_root = %s', opts.chrome_root)
159
Kuang-che Wu248c5182018-10-19 17:08:11 +0800160 if opts.dut == cros_lab_util.LAB_DUT:
161 if not opts.model and not opts.sku:
162 self.argument_parser.error(
163 'either --model or --sku need to be specified if DUT is "%s"' %
164 cros_lab_util.LAB_DUT)
165 # Board name cannot be deduced from auto allocated devices because they
166 # may be provisioned with image of unexpected board.
167 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800168 self.argument_parser.error('--board need to be specified if DUT is "%s"'
169 % cros_lab_util.LAB_DUT)
Kuang-che Wu248c5182018-10-19 17:08:11 +0800170 else:
171 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800172 opts.board = cros_util.query_dut_board(opts.dut)
173
174 if cros_util.is_cros_short_version(opts.old):
175 opts.old = cros_util.version_to_full(opts.board, opts.old)
176 if cros_util.is_cros_short_version(opts.new):
177 opts.new = cros_util.version_to_full(opts.board, opts.new)
178
179 if opts.metric:
180 if opts.old_value is None:
181 self.argument_parser.error('--old_value is not provided')
182 if opts.new_value is None:
183 self.argument_parser.error('--new_value is not provided')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800184 else:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800185 if opts.old_value is not None:
186 self.argument_parser.error(
187 '--old_value is provided but --metric is not')
188 if opts.new_value is not None:
189 self.argument_parser.error(
190 '--new_value is provided but --metric is not')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800191
Kuang-che Wua41525a2018-10-17 23:52:24 +0800192 def cmd_init(self, opts):
193 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
194 opts.mirror_base, opts.work_base, opts.session)
195 self.check_options(opts, path_factory)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800196
Kuang-che Wua41525a2018-10-17 23:52:24 +0800197 config = dict(
198 session=opts.session,
199 mirror_base=opts.mirror_base,
200 work_base=opts.work_base,
201 chromeos_root=opts.chromeos_root,
202 chromeos_mirror=opts.chromeos_mirror,
203 chrome_root=opts.chrome_root,
204 chrome_mirror=opts.chrome_mirror,
205 android_root=opts.android_root,
206 android_mirror=opts.android_mirror,
Kuang-che Wu248c5182018-10-19 17:08:11 +0800207 dut=opts.dut,
208 model=opts.model,
209 sku=opts.sku,
210 board=opts.board,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800211 old=opts.old,
212 new=opts.new,
213 test_name=opts.test_name,
214 metric=opts.metric,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800215 old_value=opts.old_value,
216 new_value=opts.new_value,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800217 noisy=opts.noisy,
218 test_that_args=opts.args,
219 always_reflash=opts.always_reflash,
220 )
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800221
Kuang-che Wua41525a2018-10-17 23:52:24 +0800222 self.states.init(config)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800223
Kuang-che Wua41525a2018-10-17 23:52:24 +0800224 # Unpack old autotest prebuilt, assume following information don't change
225 # between versions:
226 # - what chrome binaries to run
227 # - dependency labels for DUT allocation
228 common_switch_cmd, _common_eval_cmd = self._build_cmds()
229 util.check_call(*(common_switch_cmd + [self.config['old']]))
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800230
Kuang-che Wua41525a2018-10-17 23:52:24 +0800231 self.states.save()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800232
Kuang-che Wua41525a2018-10-17 23:52:24 +0800233 def _build_cmds(self):
234 # prebuilt version will be specified later.
235 common_switch_cmd = [
236 './switch_autotest_prebuilt.py',
237 '--chromeos_root', self.config['chromeos_root'],
238 '--test_name', self.config['test_name'],
239 '--board', self.config['board'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800240 ] # yapf: disable
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800241
Kuang-che Wua41525a2018-10-17 23:52:24 +0800242 common_eval_cmd = [
243 './eval_cros_autotest.py',
244 '--chromeos_root', self.config['chromeos_root'],
245 '--test_name', self.config['test_name'],
246 ] # yapf: disable
247 if self.config['metric']:
248 common_eval_cmd += [
249 '--metric', self.config['metric'],
250 '--old_value', str(self.config['old_value']),
251 '--new_value', str(self.config['new_value']),
252 ] # yapf: disable
253 if self.config['test_that_args']:
254 common_eval_cmd += ['--args', self.config['test_that_args']]
255 return common_switch_cmd, common_eval_cmd
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800256
Kuang-che Wua41525a2018-10-17 23:52:24 +0800257 def cmd_run(self, opts):
258 del opts # unused
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800259
Kuang-che Wua41525a2018-10-17 23:52:24 +0800260 self.states.load()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800261
Kuang-che Wua41525a2018-10-17 23:52:24 +0800262 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
263 self.config['mirror_base'], self.config['work_base'],
264 self.config['session'])
265 common_switch_cmd, common_eval_cmd = self._build_cmds()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800266
Kuang-che Wua41525a2018-10-17 23:52:24 +0800267 chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
268 self.config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800269
Kuang-che Wua41525a2018-10-17 23:52:24 +0800270 with cros_lab_util.dut_manager(self.config['dut'],
271 lambda: grab_dut(self.config)) as dut:
272 if not dut:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800273 raise errors.NoDutAvailable('unable to allocate DUT')
Kuang-che Wua41525a2018-10-17 23:52:24 +0800274 assert cros_util.is_dut(dut)
275 self.config['allocated_dut'] = dut
276 self.states.save()
277 common_eval_cmd.append(dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800278
Kuang-che Wua41525a2018-10-17 23:52:24 +0800279 diagnoser = diagnoser_cros.CrosDiagnoser(
280 self.config['session'], path_factory, self.config['chromeos_root'],
281 self.config['chromeos_mirror'], self.config['android_root'],
282 self.config['android_mirror'], self.config['chrome_root'],
283 self.config['chrome_mirror'], self.config['board'],
284 self.config['noisy'], dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800285
Kuang-che Wua41525a2018-10-17 23:52:24 +0800286 eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
287 # Do not specify version for autotest prebuilt switching here. The trick
288 # is that version number is obtained via bisector's environment variable
289 # CROS_VERSION.
290 extra_switch_cmd = common_switch_cmd
291 if not diagnoser.narrow_down_chromeos_prebuilt(
292 self.config['old'],
293 self.config['new'],
294 eval_cmd,
295 extra_switch_cmd=extra_switch_cmd):
296 return
297
298 diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
299 util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
300 util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
301
302 if diagnoser.narrow_down_android(eval_cmd) is not None:
303 return
304 # Assume it's ok to leave random version of android prebuilt on DUT.
305
306 # Don't --reinstall to keep chrome binaries override.
307 eval_cmd = common_eval_cmd + ['--prebuilt']
308 if diagnoser.narrow_down_chrome(
309 eval_cmd, chrome_binaries=chrome_binaries) is not None:
310 return
311
312 eval_cmd = common_eval_cmd + ['--reinstall']
313 if diagnoser.narrow_down_chromeos_localbuild(eval_cmd):
314 logger.info('%s done', __file__)
315
316 def create_argument_parser(self):
317 parser = argparse.ArgumentParser()
318 common.add_common_arguments(parser)
319 parser.add_argument('--session_base', default='bisect.sessions')
320 parser.add_argument('--session', help='Session name', required=True)
321 subparsers = parser.add_subparsers(
322 dest='command', title='commands', metavar='<command>')
323
324 parser_init = subparsers.add_parser('init', help='Initialize')
325 group = parser_init.add_argument_group(
326 title='Source tree path options',
327 description='''
328 Specify the paths of chromeos/chrome/android mirror and checkout. They
329 have the same default values as setup_cros_bisect.py, so usually you can
330 omit them and it just works.
331 ''')
332 group.add_argument(
333 '--mirror_base',
334 metavar='MIRROR_BASE',
335 default=configure.get('MIRROR_BASE',
336 setup_cros_bisect.DEFAULT_MIRROR_BASE),
337 help='Directory for mirrors (default: %(default)s)')
338 group.add_argument(
339 '--work_base',
340 metavar='WORK_BASE',
341 default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
342 help='Directory for bisection working directories '
343 '(default: %(default)s)')
344 group.add_argument(
345 '--chromeos_root',
346 metavar='CHROMEOS_ROOT',
347 type=cli.argtype_dir_path,
348 default=configure.get('CHROMEOS_ROOT'),
349 help='ChromeOS tree root')
350 group.add_argument(
351 '--chromeos_mirror',
352 type=cli.argtype_dir_path,
353 default=configure.get('CHROMEOS_MIRROR'),
354 help='ChromeOS repo mirror path')
355 group.add_argument(
356 '--android_root',
357 metavar='ANDROID_ROOT',
358 type=cli.argtype_dir_path,
359 default=configure.get('ANDROID_ROOT'),
360 help='Android tree root')
361 group.add_argument(
362 '--android_mirror',
363 type=cli.argtype_dir_path,
364 default=configure.get('ANDROID_MIRROR'),
365 help='Android repo mirror path')
366 group.add_argument(
367 '--chrome_root',
368 metavar='CHROME_ROOT',
369 type=cli.argtype_dir_path,
370 default=configure.get('CHROME_ROOT'),
371 help='Chrome tree root')
372 group.add_argument(
373 '--chrome_mirror',
374 metavar='CHROME_MIRROR',
375 type=cli.argtype_dir_path,
376 default=configure.get('CHROME_MIRROR'),
377 help="chrome's gclient cache dir")
378
Kuang-che Wu248c5182018-10-19 17:08:11 +0800379 group = parser_init.add_argument_group(title='DUT allocation options')
380 group.add_argument(
381 '--dut',
382 metavar='DUT',
383 required=True,
384 help='Address of DUT (Device Under Test). If "%s", DUT will be '
385 'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
386 group.add_argument(
387 '--model',
388 metavar='MODEL',
389 help='"model" criteria if DUT is auto allocated from the lab')
390 group.add_argument(
391 '--sku',
392 metavar='SKU',
393 help='"sku" criteria if DUT is auto allocated from the lab')
394
Kuang-che Wua41525a2018-10-17 23:52:24 +0800395 group = parser_init.add_argument_group(title='Essential options')
396 group.add_argument(
Kuang-che Wu248c5182018-10-19 17:08:11 +0800397 '--board',
398 metavar='BOARD',
399 default=configure.get('BOARD'),
400 help='ChromeOS board name; auto detected if DUT is not auto allocated')
401 group.add_argument(
Kuang-che Wua41525a2018-10-17 23:52:24 +0800402 '--old',
403 type=cros_util.argtype_cros_version,
404 required=True,
405 help='ChromeOS version with old behavior')
406 group.add_argument(
407 '--new',
408 type=cros_util.argtype_cros_version,
409 required=True,
410 help='ChromeOS version with new behavior')
411 group.add_argument('--test_name', required=True, help='Test name')
412
413 group = parser_init.add_argument_group(title='Options for benchmark test')
414 group.add_argument('--metric', help='Metric name of benchmark test')
415 group.add_argument(
416 '--old_value',
417 type=float,
418 help='For benchmark test, old value of metric')
419 group.add_argument(
420 '--new_value',
421 type=float,
422 help='For benchmark test, new value of metric')
423
Kuang-che Wua41525a2018-10-17 23:52:24 +0800424 group = parser_init.add_argument_group(title='Options passed to test_that')
425 group.add_argument(
426 '--args',
427 help='Extra args passed to "test_that --args"; Overrides the default')
428
429 group = parser_init.add_argument_group(title='Bisect behavior options')
430 group.add_argument(
431 '--noisy',
432 help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
433 group.add_argument(
434 '--always_reflash',
435 action='store_true',
436 help='Do not trust ChromeOS version number of DUT and always reflash. '
437 'This is usually only needed when resume because previous bisect was '
438 'interrupted and the DUT may be in an unexpected state')
439 parser_init.set_defaults(func=self.cmd_init)
440
441 parser_run = subparsers.add_parser('run', help='Start auto bisection')
442 parser_run.set_defaults(func=self.cmd_run)
443
444 return parser
445
446 def main(self, args=None):
447 opts = self.argument_parser.parse_args(args)
448 common.config_logging(opts)
449
450 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
451 session_file = os.path.join(session_base, opts.session,
452 self.__class__.__name__)
453 self.states = DiagnoseStates(session_file)
454 opts.func(opts)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800455
456
457if __name__ == '__main__':
Kuang-che Wua41525a2018-10-17 23:52:24 +0800458 DiagnoseCommandLine().main()