blob: 639a1c1012f6a62633429d4732c3b2bb9b43778e [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']
87 host = cros_lab_util.allocate_host(reason, config['board'], config['sku'],
88 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
150 if not opts.board:
151 if opts.dut == cros_lab_util.LAB_DUT:
152 self.argument_parser.error('--board need to be specified if DUT is "%s"'
153 % cros_lab_util.LAB_DUT)
154 else:
155 opts.board = cros_util.query_dut_board(opts.dut)
156
157 if cros_util.is_cros_short_version(opts.old):
158 opts.old = cros_util.version_to_full(opts.board, opts.old)
159 if cros_util.is_cros_short_version(opts.new):
160 opts.new = cros_util.version_to_full(opts.board, opts.new)
161
162 if opts.metric:
163 if opts.old_value is None:
164 self.argument_parser.error('--old_value is not provided')
165 if opts.new_value is None:
166 self.argument_parser.error('--new_value is not provided')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800167 else:
Kuang-che Wua41525a2018-10-17 23:52:24 +0800168 if opts.old_value is not None:
169 self.argument_parser.error(
170 '--old_value is provided but --metric is not')
171 if opts.new_value is not None:
172 self.argument_parser.error(
173 '--new_value is provided but --metric is not')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800174
Kuang-che Wua41525a2018-10-17 23:52:24 +0800175 def cmd_init(self, opts):
176 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
177 opts.mirror_base, opts.work_base, opts.session)
178 self.check_options(opts, path_factory)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800179
Kuang-che Wua41525a2018-10-17 23:52:24 +0800180 config = dict(
181 session=opts.session,
182 mirror_base=opts.mirror_base,
183 work_base=opts.work_base,
184 chromeos_root=opts.chromeos_root,
185 chromeos_mirror=opts.chromeos_mirror,
186 chrome_root=opts.chrome_root,
187 chrome_mirror=opts.chrome_mirror,
188 android_root=opts.android_root,
189 android_mirror=opts.android_mirror,
190 old=opts.old,
191 new=opts.new,
192 test_name=opts.test_name,
193 metric=opts.metric,
194 board=opts.board,
195 sku=opts.sku,
196 old_value=opts.old_value,
197 new_value=opts.new_value,
198 dut=opts.dut,
199 noisy=opts.noisy,
200 test_that_args=opts.args,
201 always_reflash=opts.always_reflash,
202 )
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800203
Kuang-che Wua41525a2018-10-17 23:52:24 +0800204 self.states.init(config)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800205
Kuang-che Wua41525a2018-10-17 23:52:24 +0800206 # Unpack old autotest prebuilt, assume following information don't change
207 # between versions:
208 # - what chrome binaries to run
209 # - dependency labels for DUT allocation
210 common_switch_cmd, _common_eval_cmd = self._build_cmds()
211 util.check_call(*(common_switch_cmd + [self.config['old']]))
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800212
Kuang-che Wua41525a2018-10-17 23:52:24 +0800213 self.states.save()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800214
Kuang-che Wua41525a2018-10-17 23:52:24 +0800215 def _build_cmds(self):
216 # prebuilt version will be specified later.
217 common_switch_cmd = [
218 './switch_autotest_prebuilt.py',
219 '--chromeos_root', self.config['chromeos_root'],
220 '--test_name', self.config['test_name'],
221 '--board', self.config['board'],
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800222 ] # yapf: disable
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800223
Kuang-che Wua41525a2018-10-17 23:52:24 +0800224 common_eval_cmd = [
225 './eval_cros_autotest.py',
226 '--chromeos_root', self.config['chromeos_root'],
227 '--test_name', self.config['test_name'],
228 ] # yapf: disable
229 if self.config['metric']:
230 common_eval_cmd += [
231 '--metric', self.config['metric'],
232 '--old_value', str(self.config['old_value']),
233 '--new_value', str(self.config['new_value']),
234 ] # yapf: disable
235 if self.config['test_that_args']:
236 common_eval_cmd += ['--args', self.config['test_that_args']]
237 return common_switch_cmd, common_eval_cmd
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800238
Kuang-che Wua41525a2018-10-17 23:52:24 +0800239 def cmd_run(self, opts):
240 del opts # unused
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800241
Kuang-che Wua41525a2018-10-17 23:52:24 +0800242 self.states.load()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800243
Kuang-che Wua41525a2018-10-17 23:52:24 +0800244 if (self.config['dut'] == cros_lab_util.LAB_DUT and
245 self.config.get('allocated_dut')):
246 self.argument_parser.error(
247 'do not resume if the dut is dynamically allocated')
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800248
Kuang-che Wua41525a2018-10-17 23:52:24 +0800249 path_factory = setup_cros_bisect.DefaultProjectPathFactory(
250 self.config['mirror_base'], self.config['work_base'],
251 self.config['session'])
252 common_switch_cmd, common_eval_cmd = self._build_cmds()
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800253
Kuang-che Wua41525a2018-10-17 23:52:24 +0800254 chrome_binaries = determine_chrome_binaries(self.config['chromeos_root'],
255 self.config['test_name'])
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800256
Kuang-che Wua41525a2018-10-17 23:52:24 +0800257 with cros_lab_util.dut_manager(self.config['dut'],
258 lambda: grab_dut(self.config)) as dut:
259 if not dut:
260 raise core.ExecutionFatalError('unable to allocate DUT')
261 assert cros_util.is_dut(dut)
262 self.config['allocated_dut'] = dut
263 self.states.save()
264 common_eval_cmd.append(dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800265
Kuang-che Wua41525a2018-10-17 23:52:24 +0800266 diagnoser = diagnoser_cros.CrosDiagnoser(
267 self.config['session'], path_factory, self.config['chromeos_root'],
268 self.config['chromeos_mirror'], self.config['android_root'],
269 self.config['android_mirror'], self.config['chrome_root'],
270 self.config['chrome_mirror'], self.config['board'],
271 self.config['noisy'], dut)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800272
Kuang-che Wua41525a2018-10-17 23:52:24 +0800273 eval_cmd = common_eval_cmd + ['--prebuilt', '--reinstall']
274 # Do not specify version for autotest prebuilt switching here. The trick
275 # is that version number is obtained via bisector's environment variable
276 # CROS_VERSION.
277 extra_switch_cmd = common_switch_cmd
278 if not diagnoser.narrow_down_chromeos_prebuilt(
279 self.config['old'],
280 self.config['new'],
281 eval_cmd,
282 extra_switch_cmd=extra_switch_cmd):
283 return
284
285 diagnoser.switch_chromeos_to_old(force=self.config['always_reflash'])
286 util.check_call(*(common_switch_cmd + [diagnoser.cros_old]))
287 util.check_call('ssh', dut, 'rm', '-rf', '/usr/local/autotest')
288
289 if diagnoser.narrow_down_android(eval_cmd) is not None:
290 return
291 # Assume it's ok to leave random version of android prebuilt on DUT.
292
293 # Don't --reinstall to keep chrome binaries override.
294 eval_cmd = common_eval_cmd + ['--prebuilt']
295 if diagnoser.narrow_down_chrome(
296 eval_cmd, chrome_binaries=chrome_binaries) is not None:
297 return
298
299 eval_cmd = common_eval_cmd + ['--reinstall']
300 if diagnoser.narrow_down_chromeos_localbuild(eval_cmd):
301 logger.info('%s done', __file__)
302
303 def create_argument_parser(self):
304 parser = argparse.ArgumentParser()
305 common.add_common_arguments(parser)
306 parser.add_argument('--session_base', default='bisect.sessions')
307 parser.add_argument('--session', help='Session name', required=True)
308 subparsers = parser.add_subparsers(
309 dest='command', title='commands', metavar='<command>')
310
311 parser_init = subparsers.add_parser('init', help='Initialize')
312 group = parser_init.add_argument_group(
313 title='Source tree path options',
314 description='''
315 Specify the paths of chromeos/chrome/android mirror and checkout. They
316 have the same default values as setup_cros_bisect.py, so usually you can
317 omit them and it just works.
318 ''')
319 group.add_argument(
320 '--mirror_base',
321 metavar='MIRROR_BASE',
322 default=configure.get('MIRROR_BASE',
323 setup_cros_bisect.DEFAULT_MIRROR_BASE),
324 help='Directory for mirrors (default: %(default)s)')
325 group.add_argument(
326 '--work_base',
327 metavar='WORK_BASE',
328 default=configure.get('WORK_BASE', setup_cros_bisect.DEFAULT_WORK_BASE),
329 help='Directory for bisection working directories '
330 '(default: %(default)s)')
331 group.add_argument(
332 '--chromeos_root',
333 metavar='CHROMEOS_ROOT',
334 type=cli.argtype_dir_path,
335 default=configure.get('CHROMEOS_ROOT'),
336 help='ChromeOS tree root')
337 group.add_argument(
338 '--chromeos_mirror',
339 type=cli.argtype_dir_path,
340 default=configure.get('CHROMEOS_MIRROR'),
341 help='ChromeOS repo mirror path')
342 group.add_argument(
343 '--android_root',
344 metavar='ANDROID_ROOT',
345 type=cli.argtype_dir_path,
346 default=configure.get('ANDROID_ROOT'),
347 help='Android tree root')
348 group.add_argument(
349 '--android_mirror',
350 type=cli.argtype_dir_path,
351 default=configure.get('ANDROID_MIRROR'),
352 help='Android repo mirror path')
353 group.add_argument(
354 '--chrome_root',
355 metavar='CHROME_ROOT',
356 type=cli.argtype_dir_path,
357 default=configure.get('CHROME_ROOT'),
358 help='Chrome tree root')
359 group.add_argument(
360 '--chrome_mirror',
361 metavar='CHROME_MIRROR',
362 type=cli.argtype_dir_path,
363 default=configure.get('CHROME_MIRROR'),
364 help="chrome's gclient cache dir")
365
366 group = parser_init.add_argument_group(title='Essential options')
367 group.add_argument(
368 '--old',
369 type=cros_util.argtype_cros_version,
370 required=True,
371 help='ChromeOS version with old behavior')
372 group.add_argument(
373 '--new',
374 type=cros_util.argtype_cros_version,
375 required=True,
376 help='ChromeOS version with new behavior')
377 group.add_argument('--test_name', required=True, help='Test name')
378
379 group = parser_init.add_argument_group(title='Options for benchmark test')
380 group.add_argument('--metric', help='Metric name of benchmark test')
381 group.add_argument(
382 '--old_value',
383 type=float,
384 help='For benchmark test, old value of metric')
385 group.add_argument(
386 '--new_value',
387 type=float,
388 help='For benchmark test, new value of metric')
389
390 group = parser_init.add_argument_group(title='DUT allocation options')
391 group.add_argument(
392 '--dut',
393 metavar='DUT',
394 required=True,
395 help='Address of DUT (Device Under Test). If "%s", DUT will be '
396 'automatically allocated from the lab' % cros_lab_util.LAB_DUT)
397 group.add_argument(
398 '--board',
399 metavar='BOARD',
400 default=configure.get('BOARD'),
401 help='ChromeOS board name')
402 group.add_argument(
403 '--sku',
404 metavar='SKU',
405 help='SKU criteria if DUT is auto allocated from the lab')
406
407 group = parser_init.add_argument_group(title='Options passed to test_that')
408 group.add_argument(
409 '--args',
410 help='Extra args passed to "test_that --args"; Overrides the default')
411
412 group = parser_init.add_argument_group(title='Bisect behavior options')
413 group.add_argument(
414 '--noisy',
415 help='Enable noisy binary search. Example value: "old=1/10,new=2/3"')
416 group.add_argument(
417 '--always_reflash',
418 action='store_true',
419 help='Do not trust ChromeOS version number of DUT and always reflash. '
420 'This is usually only needed when resume because previous bisect was '
421 'interrupted and the DUT may be in an unexpected state')
422 parser_init.set_defaults(func=self.cmd_init)
423
424 parser_run = subparsers.add_parser('run', help='Start auto bisection')
425 parser_run.set_defaults(func=self.cmd_run)
426
427 return parser
428
429 def main(self, args=None):
430 opts = self.argument_parser.parse_args(args)
431 common.config_logging(opts)
432
433 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
434 session_file = os.path.join(session_base, opts.session,
435 self.__class__.__name__)
436 self.states = DiagnoseStates(session_file)
437 opts.func(opts)
Kuang-che Wu1fcc0222018-07-07 16:43:22 +0800438
439
440if __name__ == '__main__':
Kuang-che Wua41525a2018-10-17 23:52:24 +0800441 DiagnoseCommandLine().main()