blob: 1d489383cf4b9f9732ff250087e6b0525d8cc405 [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
Kuang-che Wua41525a2018-10-17 23:52:24 +080077 reason = 'bisect-kit: %s' % config['session']
Kuang-che Wuf65c61d2018-10-19 17:48:30 +080078 if config.get('allocated_dut'):
79 host_name = cros_lab_util.dut_host_name(config['allocated_dut'])
80 logger.info('try to allocate the same host (%s) as last run', host_name)
81 host = cros_lab_util.allocate_host(reason, host=host_name)
82 else:
83 extra_labels = []
84 dependencies = info.variables.get('DEPENDENCIES', '')
85 for label in dependencies.split(','):
86 label = label.strip()
87 # Skip non-machine labels
88 if label in ['cleanup-reboot']:
89 continue
90 extra_labels.append(label)
91
92 host = cros_lab_util.allocate_host(
93 reason,
94 model=config['model'],
95 sku=config['sku'],
96 extra_labels=extra_labels)
97
Kuang-che Wu1fcc0222018-07-07 16:43:22 +080098 if not host:
99 logger.error('unable to allocate dut')
100 return None
101
102 logger.info('allocated host %s', host)
103 return host
104
105
106def may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
107 info = cros_util.get_autotest_test_info(autotest_dir, test_name)
108 assert info
109 dirpath = os.path.dirname(info.path)
110 for pypath in glob.glob(os.path.join(dirpath, '*.py')):
111 if 'ChromeBinaryTest' in open(pypath).read():
112 return True
113 return False
114
115
Kuang-che Wua41525a2018-10-17 23:52:24 +0800116def determine_chrome_binaries(chromeos_root, test_name):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800117 chrome_binaries = None
118 for name_pattern, binaries in CHROME_BINARIES_OF_TEST.items():
Kuang-che Wua41525a2018-10-17 23:52:24 +0800119 if fnmatch.fnmatch(test_name, name_pattern):
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800120 chrome_binaries = binaries
121 break
122
Kuang-che Wua41525a2018-10-17 23:52:24 +0800123 autotest_dir = os.path.join(chromeos_root, cros_util.prebuilt_autotest_dir)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800124 if chrome_binaries:
125 logger.info('This test depends on chrome binary: %s', chrome_binaries)
Kuang-che Wua41525a2018-10-17 23:52:24 +0800126 elif may_depend_on_extra_chrome_binaries(autotest_dir, test_name):
Kuang-che Wu74768d32018-09-07 12:03:24 +0800127 logger.warning(
128 '%s code used ChromeBinaryTest but the binary is unknown; '
Kuang-che Wua41525a2018-10-17 23:52:24 +0800129 'please update CHROME_BINARIES_OF_TEST table', test_name)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800130 return chrome_binaries
131
132
Kuang-che Wua41525a2018-10-17 23:52:24 +0800133class DiagnoseCommandLine(object):
134 """Diagnose command line interface."""
Kuang-che Wud8fc9572018-10-03 21:00:41 +0800135
Kuang-che Wua41525a2018-10-17 23:52:24 +0800136 def __init__(self):
137 common.init()
138 self.argument_parser = self.create_argument_parser()
139 self.states = None
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800140
Kuang-che Wua41525a2018-10-17 23:52:24 +0800141 @property
142 def config(self):
143 return self.states.config
144
145 def check_options(self, opts, path_factory):
146 if not opts.chromeos_mirror:
147 opts.chromeos_mirror = path_factory.get_chromeos_mirror()
148 logger.info('chromeos_mirror = %s', opts.chromeos_mirror)
149 if not opts.chromeos_root:
150 opts.chromeos_root = path_factory.get_chromeos_tree()
151 logger.info('chromeos_root = %s', opts.chromeos_root)
152 if not opts.chrome_mirror:
153 opts.chrome_mirror = path_factory.get_chrome_cache()
154 logger.info('chrome_mirror = %s', opts.chrome_mirror)
155 if not opts.chrome_root:
156 opts.chrome_root = path_factory.get_chrome_tree()
157 logger.info('chrome_root = %s', opts.chrome_root)
158
Kuang-che Wu248c5182018-10-19 17:08:11 +0800159 if opts.dut == cros_lab_util.LAB_DUT:
160 if not opts.model and not opts.sku:
161 self.argument_parser.error(
162 'either --model or --sku need to be specified if DUT is "%s"' %
163 cros_lab_util.LAB_DUT)
164 # Board name cannot be deduced from auto allocated devices because they
165 # may be provisioned with image of unexpected board.
166 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800167 self.argument_parser.error('--board need to be specified if DUT is "%s"'
168 % cros_lab_util.LAB_DUT)
Kuang-che Wu248c5182018-10-19 17:08:11 +0800169 else:
170 if not opts.board:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800171 opts.board = cros_util.query_dut_board(opts.dut)
172
173 if cros_util.is_cros_short_version(opts.old):
174 opts.old = cros_util.version_to_full(opts.board, opts.old)
175 if cros_util.is_cros_short_version(opts.new):
176 opts.new = cros_util.version_to_full(opts.board, opts.new)
177
178 if opts.metric:
179 if opts.old_value is None:
180 self.argument_parser.error('--old_value is not provided')
181 if opts.new_value is None:
182 self.argument_parser.error('--new_value is not provided')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800183 else:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800184 if opts.old_value is not None:
185 self.argument_parser.error(
186 '--old_value is provided but --metric is not')
187 if opts.new_value is not None:
188 self.argument_parser.error(
189 '--new_value is provided but --metric is not')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800190
Kuang-che Wua41525a2018-10-17 23:52:24 +0800191 def cmd_init(self, opts):
192 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
193 opts.mirror_base, opts.work_base, opts.session)
194 self.check_options(opts, path_factory)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800195
Kuang-che Wua41525a2018-10-17 23:52:24 +0800196 config = dict(
197 session=opts.session,
198 mirror_base=opts.mirror_base,
199 work_base=opts.work_base,
200 chromeos_root=opts.chromeos_root,
201 chromeos_mirror=opts.chromeos_mirror,
202 chrome_root=opts.chrome_root,
203 chrome_mirror=opts.chrome_mirror,
204 android_root=opts.android_root,
205 android_mirror=opts.android_mirror,
Kuang-che Wu248c5182018-10-19 17:08:11 +0800206 dut=opts.dut,
207 model=opts.model,
208 sku=opts.sku,
209 board=opts.board,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800210 old=opts.old,
211 new=opts.new,
212 test_name=opts.test_name,
213 metric=opts.metric,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800214 old_value=opts.old_value,
215 new_value=opts.new_value,
Kuang-che Wua41525a2018-10-17 23:52:24 +0800216 noisy=opts.noisy,
217 test_that_args=opts.args,
218 always_reflash=opts.always_reflash,
219 )
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800220
Kuang-che Wua41525a2018-10-17 23:52:24 +0800221 self.states.init(config)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800222
Kuang-che Wua41525a2018-10-17 23:52:24 +0800223 # Unpack old autotest prebuilt, assume following information don't change
224 # between versions:
225 # - what chrome binaries to run
226 # - dependency labels for DUT allocation
227 common_switch_cmd, _common_eval_cmd = self._build_cmds()
228 util.check_call(*(common_switch_cmd + [self.config['old']]))
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800229
Kuang-che Wua41525a2018-10-17 23:52:24 +0800230 self.states.save()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800231
Kuang-che Wua41525a2018-10-17 23:52:24 +0800232 def _build_cmds(self):
233 # prebuilt version will be specified later.
234 common_switch_cmd = [
235 './switch_autotest_prebuilt.py',
236 '--chromeos_root', self.config['chromeos_root'],
237 '--test_name', self.config['test_name'],
238 '--board', self.config['board'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800239 ] # yapf: disable
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800240
Kuang-che Wua41525a2018-10-17 23:52:24 +0800241 common_eval_cmd = [
242 './eval_cros_autotest.py',
243 '--chromeos_root', self.config['chromeos_root'],
244 '--test_name', self.config['test_name'],
245 ] # yapf: disable
246 if self.config['metric']:
247 common_eval_cmd += [
248 '--metric', self.config['metric'],
249 '--old_value', str(self.config['old_value']),
250 '--new_value', str(self.config['new_value']),
251 ] # yapf: disable
252 if self.config['test_that_args']:
253 common_eval_cmd += ['--args', self.config['test_that_args']]
254 return common_switch_cmd, common_eval_cmd
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800255
Kuang-che Wua41525a2018-10-17 23:52:24 +0800256 def cmd_run(self, opts):
257 del opts # unused
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800258
Kuang-che Wua41525a2018-10-17 23:52:24 +0800259 self.states.load()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800260
Kuang-che Wua41525a2018-10-17 23:52:24 +0800261 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
262 self.config['mirror_base'], self.config['work_base'],
263 self.config['session'])
264 common_switch_cmd, common_eval_cmd = self._build_cmds()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800265
Kuang-che Wua41525a2018-10-17 23:52:24 +0800266 chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
267 self.config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800268
Kuang-che Wua41525a2018-10-17 23:52:24 +0800269 with cros_lab_util.dut_manager(self.config['dut'],
270 lambda: grab_dut(self.config)) as dut:
271 if not dut:
272 raise core.ExecutionFatalError('unable to allocate DUT')
273 assert cros_util.is_dut(dut)
274 self.config['allocated_dut'] = dut
275 self.states.save()
276 common_eval_cmd.append(dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800277
Kuang-che Wua41525a2018-10-17 23:52:24 +0800278 diagnoser = diagnoser_cros.CrosDiagnoser(
279 self.config['session'], path_factory, self.config['chromeos_root'],
280 self.config['chromeos_mirror'], self.config['android_root'],
281 self.config['android_mirror'], self.config['chrome_root'],
282 self.config['chrome_mirror'], self.config['board'],
283 self.config['noisy'], dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800284
Kuang-che Wua41525a2018-10-17 23:52:24 +0800285 eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
286 # Do not specify version for autotest prebuilt switching here. The trick
287 # is that version number is obtained via bisector's environment variable
288 # CROS_VERSION.
289 extra_switch_cmd = common_switch_cmd
290 if not diagnoser.narrow_down_chromeos_prebuilt(
291 self.config['old'],
292 self.config['new'],
293 eval_cmd,
294 extra_switch_cmd=extra_switch_cmd):
295 return
296
297 diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
298 util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
299 util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
300
301 if diagnoser.narrow_down_android(eval_cmd) is not None:
302 return
303 # Assume it's ok to leave random version of android prebuilt on DUT.
304
305 # Don't --reinstall to keep chrome binaries override.
306 eval_cmd = common_eval_cmd + ['--prebuilt']
307 if diagnoser.narrow_down_chrome(
308 eval_cmd, chrome_binaries=chrome_binaries) is not None:
309 return
310
311 eval_cmd = common_eval_cmd + ['--reinstall']
312 if diagnoser.narrow_down_chromeos_localbuild(eval_cmd):
313 logger.info('%s done', __file__)
314
315 def create_argument_parser(self):
316 parser = argparse.ArgumentParser()
317 common.add_common_arguments(parser)
318 parser.add_argument('--session_base', default='bisect.sessions')
319 parser.add_argument('--session', help='Session name', required=True)
320 subparsers = parser.add_subparsers(
321 dest='command', title='commands', metavar='<command>')
322
323 parser_init = subparsers.add_parser('init', help='Initialize')
324 group = parser_init.add_argument_group(
325 title='Source tree path options',
326 description='''
327 Specify the paths of chromeos/chrome/android mirror and checkout. They
328 have the same default values as setup_cros_bisect.py, so usually you can
329 omit them and it just works.
330 ''')
331 group.add_argument(
332 '--mirror_base',
333 metavar='MIRROR_BASE',
334 default=configure.get('MIRROR_BASE',
335 setup_cros_bisect.DEFAULT_MIRROR_BASE),
336 help='Directory for mirrors (default: %(default)s)')
337 group.add_argument(
338 '--work_base',
339 metavar='WORK_BASE',
340 default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
341 help='Directory for bisection working directories '
342 '(default: %(default)s)')
343 group.add_argument(
344 '--chromeos_root',
345 metavar='CHROMEOS_ROOT',
346 type=cli.argtype_dir_path,
347 default=configure.get('CHROMEOS_ROOT'),
348 help='ChromeOS tree root')
349 group.add_argument(
350 '--chromeos_mirror',
351 type=cli.argtype_dir_path,
352 default=configure.get('CHROMEOS_MIRROR'),
353 help='ChromeOS repo mirror path')
354 group.add_argument(
355 '--android_root',
356 metavar='ANDROID_ROOT',
357 type=cli.argtype_dir_path,
358 default=configure.get('ANDROID_ROOT'),
359 help='Android tree root')
360 group.add_argument(
361 '--android_mirror',
362 type=cli.argtype_dir_path,
363 default=configure.get('ANDROID_MIRROR'),
364 help='Android repo mirror path')
365 group.add_argument(
366 '--chrome_root',
367 metavar='CHROME_ROOT',
368 type=cli.argtype_dir_path,
369 default=configure.get('CHROME_ROOT'),
370 help='Chrome tree root')
371 group.add_argument(
372 '--chrome_mirror',
373 metavar='CHROME_MIRROR',
374 type=cli.argtype_dir_path,
375 default=configure.get('CHROME_MIRROR'),
376 help="chrome's gclient cache dir")
377
Kuang-che Wu248c5182018-10-19 17:08:11 +0800378 group = parser_init.add_argument_group(title='DUT allocation options')
379 group.add_argument(
380 '--dut',
381 metavar='DUT',
382 required=True,
383 help='Address of DUT (Device Under Test). If "%s", DUT will be '
384 'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
385 group.add_argument(
386 '--model',
387 metavar='MODEL',
388 help='"model" criteria if DUT is auto allocated from the lab')
389 group.add_argument(
390 '--sku',
391 metavar='SKU',
392 help='"sku" criteria if DUT is auto allocated from the lab')
393
Kuang-che Wua41525a2018-10-17 23:52:24 +0800394 group = parser_init.add_argument_group(title='Essential options')
395 group.add_argument(
Kuang-che Wu248c5182018-10-19 17:08:11 +0800396 '--board',
397 metavar='BOARD',
398 default=configure.get('BOARD'),
399 help='ChromeOS board name; auto detected if DUT is not auto allocated')
400 group.add_argument(
Kuang-che Wua41525a2018-10-17 23:52:24 +0800401 '--old',
402 type=cros_util.argtype_cros_version,
403 required=True,
404 help='ChromeOS version with old behavior')
405 group.add_argument(
406 '--new',
407 type=cros_util.argtype_cros_version,
408 required=True,
409 help='ChromeOS version with new behavior')
410 group.add_argument('--test_name', required=True, help='Test name')
411
412 group = parser_init.add_argument_group(title='Options for benchmark test')
413 group.add_argument('--metric', help='Metric name of benchmark test')
414 group.add_argument(
415 '--old_value',
416 type=float,
417 help='For benchmark test, old value of metric')
418 group.add_argument(
419 '--new_value',
420 type=float,
421 help='For benchmark test, new value of metric')
422
Kuang-che Wua41525a2018-10-17 23:52:24 +0800423 group = parser_init.add_argument_group(title='Options passed to test_that')
424 group.add_argument(
425 '--args',
426 help='Extra args passed to "test_that --args"; Overrides the default')
427
428 group = parser_init.add_argument_group(title='Bisect behavior options')
429 group.add_argument(
430 '--noisy',
431 help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
432 group.add_argument(
433 '--always_reflash',
434 action='store_true',
435 help='Do not trust ChromeOS version number of DUT and always reflash. '
436 'This is usually only needed when resume because previous bisect was '
437 'interrupted and the DUT may be in an unexpected state')
438 parser_init.set_defaults(func=self.cmd_init)
439
440 parser_run = subparsers.add_parser('run', help='Start auto bisection')
441 parser_run.set_defaults(func=self.cmd_run)
442
443 return parser
444
445 def main(self, args=None):
446 opts = self.argument_parser.parse_args(args)
447 common.config_logging(opts)
448
449 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
450 session_file = os.path.join(session_base, opts.session,
451 self.__class__.__name__)
452 self.states = DiagnoseStates(session_file)
453 opts.func(opts)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800454
455
456if __name__ == '__main__':
Kuang-che Wua41525a2018-10-17 23:52:24 +0800457 DiagnoseCommandLine().main()