blob: 63ee34e22a5c882f21a8b2f541ef76a460203408 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu88875db2017-07-20 10:47:53 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Bisect command line interface."""
6
7from __future__ import print_function
8import argparse
9import datetime
Kuang-che Wu8b654092018-11-09 17:56:25 +080010import json
Kuang-che Wu88875db2017-07-20 10:47:53 +080011import logging
12import os
13import re
Kuang-che Wu443633f2019-02-27 00:58:33 +080014import signal
Kuang-che Wu88875db2017-07-20 10:47:53 +080015import textwrap
16import time
17
18from bisect_kit import common
Kuang-che Wu385279d2017-09-27 14:48:28 +080019from bisect_kit import configure
Kuang-che Wu88875db2017-07-20 10:47:53 +080020from bisect_kit import core
Kuang-che Wue121fae2018-11-09 16:18:39 +080021from bisect_kit import errors
Kuang-che Wu88875db2017-07-20 10:47:53 +080022from bisect_kit import strategy
23from bisect_kit import util
24
25logger = logging.getLogger(__name__)
26
Kuang-che Wu88875db2017-07-20 10:47:53 +080027DEFAULT_SESSION_NAME = 'default'
28DEFAULT_CONFIDENCE = 0.999
29
Kuang-che Wu0476d1f2019-03-04 19:27:01 +080030# Exit code of bisect eval script. These values are chosen compatible with 'git
31# bisect'.
32EXIT_CODE_OLD = 0
33EXIT_CODE_NEW = 1
34EXIT_CODE_SKIP = 125
35EXIT_CODE_FATAL = 128
36
Kuang-che Wu88875db2017-07-20 10:47:53 +080037
38class ArgTypeError(argparse.ArgumentTypeError):
39 """An error for argument validation failure.
40
41 This not only tells users the argument is wrong but also gives correct
42 example. The main purpose of this error is for argtype_multiplexer, which
43 cascades examples from multiple ArgTypeError.
44 """
45
46 def __init__(self, msg, example):
47 self.msg = msg
48 if isinstance(example, list):
49 self.example = example
50 else:
51 self.example = [example]
52 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
53 super(ArgTypeError, self).__init__(full_msg)
54
55
56def argtype_notempty(s):
57 """Validates argument is not an empty string.
58
59 Args:
60 s: string to validate.
61
62 Raises:
63 ArgTypeError if argument is empty string.
64 """
65 if not s:
66 msg = 'should not be empty'
67 raise ArgTypeError(msg, 'foo')
68 return s
69
70
71def argtype_int(s):
72 """Validate argument is a number.
73
74 Args:
75 s: string to validate.
76
77 Raises:
78 ArgTypeError if argument is not a number.
79 """
80 try:
81 return str(int(s))
82 except ValueError:
83 raise ArgTypeError('should be a number', '123')
84
85
Kuang-che Wu603cdad2019-01-18 21:32:55 +080086def argtype_re(pattern, example):
87 r"""Validate argument matches `pattern`.
88
89 Args:
90 pattern: regex pattern
91 example: example string which matches `pattern`
92
93 Returns:
94 A new argtype function which matches regex `pattern`
95 """
96 assert re.match(pattern, example)
97
98 def validate(s):
99 if re.match(pattern, s):
100 return s
101 if re.escape(pattern) == pattern:
102 raise ArgTypeError('should be "%s"' % pattern, pattern)
103 raise ArgTypeError('should match "%s"' % pattern,
104 '"%s" like %s' % (pattern, example))
105
106 return validate
107
108
Kuang-che Wu88875db2017-07-20 10:47:53 +0800109def argtype_multiplexer(*args):
110 r"""argtype multiplexer
111
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800112 This function takes a list of argtypes and creates a new function matching
113 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800114
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800115 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800116 >>> argtype = argtype_multiplexer(argtype_int,
117 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800118 >>> argtype('123')
119 123
120 >>> argtype('r456')
121 r456
122 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800123 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800124
125 Args:
126 *args: list of argtypes or regex pattern.
127
128 Returns:
129 A new argtype function which matches *args.
130 """
131
132 def validate(s):
133 examples = []
134 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800135 try:
136 return t(s)
137 except ArgTypeError as e:
138 examples += e.example
139
140 msg = 'Invalid argument'
141 raise ArgTypeError(msg, examples)
142
143 return validate
144
145
146def argtype_multiplier(argtype):
147 """A new argtype that supports multiplier suffix of the given argtype.
148
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800149 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800150 Supports the given argtype accepting "foo" as argument, this function
151 generates a new argtype function which accepts argument like "foo*3".
152
153 Returns:
154 A new argtype function which returns (arg, times) where arg is accepted
155 by input `argtype` and times is repeating count. Note that if multiplier is
156 omitted, "times" is 1.
157 """
158
159 def helper(s):
160 m = re.match(r'^(.+)\*(\d+)$', s)
161 try:
162 if m:
163 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800164 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800165 except ArgTypeError as e:
166 # It should be okay to gives multiplier example only for the first one
167 # because it is just "example", no need to enumerate all possibilities.
168 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
169
170 return helper
171
172
173def argtype_dir_path(s):
174 """Validate argument is an existing directory.
175
176 Args:
177 s: string to validate.
178
179 Raises:
180 ArgTypeError if the path is not a directory.
181 """
182 if not os.path.exists(s):
183 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
184 if not os.path.isdir(s):
185 raise ArgTypeError('should be a directory', '/path/to/somewhere')
186
187 # Normalize, trim trailing path separators.
188 if len(s) > 1 and s[-1] == os.path.sep:
189 s = s[:-1]
190 return s
191
192
193def _collect_bisect_result_values(values, line):
194 """Collect bisect result values from output line.
195
196 Args:
197 values: Collected values are appending to this list.
198 line: One line of output string.
199 """
200 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
201 if m:
202 try:
203 values.extend(map(float, m.group(1).split()))
204 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800205 raise errors.InternalError(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800206 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
207
208
Kuang-che Wu88518882017-09-22 16:57:25 +0800209def check_executable(program):
210 """Checks whether a program is executable.
211
212 Args:
213 program: program path in question
214
215 Returns:
216 string as error message if `program` is not executable, or None otherwise.
217 It will return None if unable to determine as well.
218 """
219 returncode = util.call('which', program)
220 if returncode == 127: # No 'which' on this platform, skip the check.
221 return None
222 if returncode == 0: # is executable
223 return None
224
225 hint = ''
226 if not os.path.exists(program):
227 hint = 'Not in PATH?'
228 elif not os.path.isfile(program):
229 hint = 'Not a file'
230 elif not os.access(program, os.X_OK):
231 hint = 'Forgot to chmod +x?'
232 elif '/' not in program:
233 hint = 'Forgot to prepend "./" ?'
234 return '%r is not executable. %s' % (program, hint)
235
236
Kuang-che Wu443633f2019-02-27 00:58:33 +0800237def format_returncode(returncode):
238 if returncode < 0:
239 signum = -returncode
240 signame = 'Unknown'
241 for k, v in vars(signal).items():
242 if k.startswith('SIG') and '_' not in k and v == signum:
243 signame = k
244 return 'terminated by signal %d (%s)' % (signum, signame)
245
246 return 'exited with code %d' % returncode
247
248
Kuang-che Wu88875db2017-07-20 10:47:53 +0800249def do_evaluate(evaluate_cmd, domain, rev):
250 """Invokes evaluator command.
251
252 The `evaluate_cmd` can get the target revision from the environment variable
253 named 'BISECT_REV'.
254
255 The result is determined according to the exit code of evaluator:
256 0: 'old'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800257 1..124, 126, 127: 'new'
Kuang-che Wu88875db2017-07-20 10:47:53 +0800258 125: 'skip'
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800259 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800260 terminated by signal: fatal error
261
262 p.s. the definition of result is compatible with git-bisect(1).
263
264 It also extracts additional values from evaluate_cmd's stdout lines which
265 match the following format:
266 BISECT_RESULT_VALUES=<float>[, <float>]*
267
268 Args:
269 evaluate_cmd: evaluator command.
270 domain: a bisect_kit.core.Domain instance.
271 rev: version to evaluate.
272
273 Returns:
274 (result, values):
275 result is one of 'old', 'new', 'skip'.
276 values are additional collected values, like performance score.
277
278 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800279 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800280 """
281 env = os.environ.copy()
282 env['BISECT_REV'] = rev
283 domain.setenv(env, rev)
284
285 values = []
286 p = util.Popen(
287 evaluate_cmd,
288 env=env,
289 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
290 returncode = p.wait()
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800291 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800292 raise errors.ExecutionFatalError(
293 'eval failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800294
295 if returncode == 0:
296 return 'old', values
297 if returncode == 125:
298 return 'skip', values
299 return 'new', values
300
301
302def do_switch(switch_cmd, domain, rev):
303 """Invokes switcher command.
304
305 The `switch_cmd` can get the target revision from the environment variable
306 named 'BISECT_REV'.
307
308 The result is determined according to the exit code of switcher:
309 0: switch succeeded
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800310 1..127: 'skip'
311 128..255: fatal error
Kuang-che Wu88875db2017-07-20 10:47:53 +0800312 terminated by signal: fatal error
313
314 In other words, any non-fatal errors are considered as 'skip'.
315
316 Args:
317 switch_cmd: switcher command.
318 domain: a bisect_kit.core.Domain instance.
319 rev: version to switch.
320
321 Returns:
322 None if switch successfully, 'skip' otherwise.
323
324 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800325 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800326 """
327 env = os.environ.copy()
328 env['BISECT_REV'] = rev
329 domain.setenv(env, rev)
330
331 returncode = util.call(*switch_cmd, env=env)
Kuang-che Wu0476d1f2019-03-04 19:27:01 +0800332 if returncode < 0 or returncode >= 128:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800333 raise errors.ExecutionFatalError(
334 'switch failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800335
336 if returncode != 0:
337 return 'skip'
338 return None
339
340
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800341class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800342 """Bisector command line interface.
343
344 The typical usage pattern:
345
346 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800347 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800348
349 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800350 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800351
352 If you need to control the bisector using python code, the easier way is
353 passing command line arguments to main() function. For example,
354 bisector = Bisector(CustomDomain)
355 bisector.main('init', '--old', '123', '--new', '456')
356 bisector.main('config', 'switch', 'true')
357 bisector.main('config', 'eval', 'true')
358 bisector.main('run')
359 """
360
361 def __init__(self, domain_cls):
362 self.domain_cls = domain_cls
363 self.domain = None
364 self.states = None
365 self.strategy = None
366
367 @property
368 def config(self):
369 return self.states.config
370
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800371 def _format_status(self, status):
372 if status in ('old', 'new'):
Kuang-che Wub6756d42019-01-25 12:19:55 +0800373 return '%s behavior' % status
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800374 return status
375
Kuang-che Wu8b654092018-11-09 17:56:25 +0800376 def _add_sample(self, rev, status, **kwargs):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800377 idx = self.states.rev2idx(rev)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800378 self.states.add_sample(idx, status, **kwargs)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800379 self.strategy.update(idx, status)
380
381 def cmd_reset(self, _opts):
382 """Resets bisect session and clean up saved result."""
383 self.states.reset()
384
385 def cmd_init(self, opts):
386 """Initializes bisect session.
387
388 See init command's help message for more detail.
389 """
390 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800391 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800392 logger.debug('revlist %r', revlist)
393 if 'new' not in config:
394 config['new'] = opts.new
395 if 'old' not in config:
396 config['old'] = opts.old
397 assert len(revlist) >= 2
398 assert config['new'] in revlist
399 assert config['old'] in revlist
400 old_idx = revlist.index(config['old'])
401 new_idx = revlist.index(config['new'])
402 assert old_idx < new_idx
403
Kuang-che Wu81cde452019-04-08 16:56:51 +0800404 config.update(
405 confidence=opts.confidence,
406 noisy=opts.noisy,
407 old_value=opts.old_value,
408 new_value=opts.new_value)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800409
410 self.states.init(config, revlist)
411 self.states.save()
412
413 def _switch_and_eval(self, rev, prev_rev=None):
414 """Switches and evaluates given version.
415
416 If current version equals to target, switch step will be skip.
417
418 Args:
419 rev: Target version.
420 prev_rev: Previous version.
421
422 Returns:
423 (step, status, values):
424 step: Last step executed ('switch' or 'eval').
425 status: Execution result ('old', 'new', or 'skip').
426 values: Collected values from eval step. None if last step is 'switch'.
427 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800428 if prev_rev != rev:
429 logger.debug('switch to rev=%s', rev)
430 t0 = time.time()
431 status = do_switch(self.config['switch'], self.domain, rev)
432 t1 = time.time()
433 if status == 'skip':
434 logger.debug('switch failed => skip')
435 return 'switch', status, None
436 self.states.data['stats']['switch_count'] += 1
437 self.states.data['stats']['switch_time'] += t1 - t0
438
439 logger.debug('eval rev=%s', rev)
440 t0 = time.time()
441 status, values = do_evaluate(self.config['eval'], self.domain, rev)
442 t1 = time.time()
443 if status == 'skip':
444 return 'eval', status, values
Kuang-che Wu81cde452019-04-08 16:56:51 +0800445
446 if self.strategy.is_value_bisection():
Kuang-che Wuc986b1d2019-04-15 16:45:20 +0800447 if not values:
448 raise errors.ExecutionFatalError(
449 'eval command (%s) terminated normally but did not output values' %
450 self.config['evaluate_cmd'])
Kuang-che Wu81cde452019-04-08 16:56:51 +0800451 status = self.strategy.classify_result_from_values(values)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800452 self.states.data['stats']['eval_count'] += 1
453 self.states.data['stats']['eval_time'] += t1 - t0
454
455 return 'eval', status, values
456
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800457 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800458 if opts.revs:
459 for rev in opts.revs:
460 idx = self.states.rev2idx(rev)
461 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
462 yield idx, rev
463 if opts.once:
464 break
465 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800466 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800467 idx = self.strategy.next_idx()
468 rev = self.states.idx2rev(idx)
469 logger.info('try idx=%d rev=%s', idx, rev)
470 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800471 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800472 if opts.once:
473 break
474
475 def cmd_run(self, opts):
476 """Performs bisection.
477
478 See run command's help message for more detail.
479
480 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800481 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800482 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800483 errors.UnableToProceed: Too many errors to narrow down further the
484 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800485 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800486 # Set dummy values in case exception raised before loop.
487 idx, rev = -1, None
488 try:
489 assert self.config.get('switch')
490 assert self.config.get('eval')
491 self.strategy.rebuild()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800492
Kuang-che Wu8b654092018-11-09 17:56:25 +0800493 prev_rev = None
494 force = opts.force
495 for idx, rev in self._next_idx_iter(opts, force):
496 if not force:
497 # Bail out if bisection range is unlikely true in order to prevent
498 # wasting time. This is necessary because some configurations (say,
499 # confidence) may be changed before cmd_run() and thus the bisection
500 # range becomes not acceptable.
501 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800502
Kuang-che Wu8b654092018-11-09 17:56:25 +0800503 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
Kuang-che Wu81cde452019-04-08 16:56:51 +0800504 if self.strategy.is_value_bisection():
505 logger.info('rev=%s status => %s: %s', rev,
506 self._format_status(status), values)
507 else:
508 logger.info('rev=%s status => %s', rev, self._format_status(status))
Kuang-che Wu8b654092018-11-09 17:56:25 +0800509 force = False
510
Kuang-che Wu978b65a2019-03-12 09:50:40 +0800511 self.states.add_sample(idx, status, values=values)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800512 self.states.save()
513
514 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800515 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800516
Kuang-che Wu978b65a2019-03-12 09:50:40 +0800517 self.strategy.update(idx, status)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800518 self.strategy.show_summary()
519
520 if step == 'switch' and status == 'skip':
521 # Previous switch failed and thus the current version is unknown. Set
522 # it None, so next switch operation won't be bypassed (due to
523 # optimization).
524 prev_rev = None
525 else:
526 prev_rev = rev
527
528 logger.info('done')
529 old_idx, new_idx = self.strategy.get_range()
530 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800531 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800532 except Exception as e:
533 exception_name = e.__class__.__name__
534 self.states.add_history(
535 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
536 self.states.save()
537 raise
538 finally:
Kuang-che Wu978b65a2019-03-12 09:50:40 +0800539 if rev and sum(self.strategy.prob) > 0:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800540 # progress so far
541 old_idx, new_idx = self.strategy.get_range()
542 self.states.add_history(
543 'range',
544 old=self.states.idx2rev(old_idx),
545 new=self.states.idx2rev(new_idx))
546 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800547
548 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800549 """Shows remaining candidates."""
Kuang-che Wu15874b62019-01-11 21:10:27 +0800550 try:
551 self.strategy.rebuild()
552 # Rebuild twice in order to re-estimate noise.
553 self.strategy.rebuild()
554 except errors.VerificationFailed:
555 # Do nothing, go ahead to show existing information anyway.
556 pass
Kuang-che Wue80bb872018-11-15 19:45:25 +0800557
558 old_idx, new_idx = self.strategy.get_range()
559 old, new = map(self.states.idx2rev, [old_idx, new_idx])
560 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
561 self.strategy.confidence / 10.0)
562 summary = {
563 'rev_info': [vars(info).copy() for info in self.states.rev_info],
564 'current_range': (old, new),
565 'highlight_range':
566 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
567 'prob':
568 self.strategy.prob,
569 'remaining_steps':
570 self.strategy.remaining_steps(),
571 }
572
573 if opts.verbose or opts.json:
574 interesting_indexes = set(range(len(summary['rev_info'])))
575 else:
576 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800577 if self.strategy.prob:
578 for i, p in enumerate(self.strategy.prob):
579 if p > 0.05:
580 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800581
582 self.domain.fill_candidate_summary(summary, interesting_indexes)
583
584 if opts.json:
585 print(json.dumps(summary, indent=2, sort_keys=True))
586 else:
587 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
588
589 def show_summary(self, summary, interesting_indexes, verbose=False):
590 old, new = summary['current_range']
591 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
592
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800593 for link in summary.get('links', []):
594 print('%s: %s' % (link['name'], link['url']))
595 if 'note' in link:
596 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800597
598 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800599 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800600 print('(roughly %d steps)' % summary['remaining_steps'])
601
602 for i, rev_info in enumerate(summary['rev_info']):
603 if (not verbose and not old_idx <= i <= new_idx and
604 not rev_info['result_counter']):
605 continue
606
607 detail = []
Kuang-che Wu05e416e2019-02-21 12:33:52 +0800608 if self.strategy.is_noisy() and summary['prob']:
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800609 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800610 if rev_info['result_counter']:
611 detail.append(str(rev_info['result_counter']))
612 values = sorted(rev_info['values'])
613 if len(values) == 1:
614 detail.append('%.3f' % values[0])
615 elif len(values) > 1:
616 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
617 (len(values), sum(values) / len(values),
618 values[len(values) // 2], values[0], values[-1]))
619
620 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
621 if i in interesting_indexes:
622 if 'comment' in rev_info:
623 print('\t%s' % rev_info['comment'])
624 for action in rev_info.get('actions', []):
625 if 'text' in action:
626 print('\t%s' % action['text'])
627 if 'link' in action:
628 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800629
630 def current_status(self, session=None, session_base=None):
631 """Gets current bisect status.
632
633 Returns:
634 A dict describing current status. It contains following items:
635 inited: True iff the session file is initialized (init command has been
636 invoked). If not, below items are omitted.
637 old: Start of current estimated range.
638 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800639 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800640 estimated_noise: New estimated noise.
641 done: True if bisection is done, otherwise False.
642 """
643 self._create_states(session=session, session_base=session_base)
644 if self.states.load():
645 self.strategy = strategy.NoisyBinarySearch(
646 self.states.rev_info,
647 self.states.rev2idx(self.config['old']),
648 self.states.rev2idx(self.config['new']),
Kuang-che Wu81cde452019-04-08 16:56:51 +0800649 old_value=self.config['old_value'],
650 new_value=self.config['new_value'],
Kuang-che Wu88875db2017-07-20 10:47:53 +0800651 confidence=self.config['confidence'],
652 observation=self.config['noisy'])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800653 try:
654 self.strategy.rebuild()
655 except errors.VerificationFailed:
656 # Do nothing, go ahead to show existing information anyway.
657 pass
Kuang-che Wu88875db2017-07-20 10:47:53 +0800658 left, right = self.strategy.get_range()
659 estimated_noise = self.strategy.get_noise_observation()
660
661 result = dict(
662 inited=True,
663 old=self.states.idx2rev(left),
664 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800665 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800666 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800667 done=self.strategy.is_done())
668 else:
669 result = dict(inited=False)
670 return result
671
Kuang-che Wu8b654092018-11-09 17:56:25 +0800672 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800673 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800674 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800675 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800676 if opts.before and entry['timestamp'] >= opts.before:
677 continue
678 if opts.after and entry['timestamp'] <= opts.after:
679 continue
680 history.append(entry)
681
682 if opts.json:
683 print(json.dumps(history, indent=2))
684 return
685
686 for entry in history:
687 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
688 if entry.get('event', 'sample') == 'sample':
689 print('{datetime} {rev} {status} {values} {comment}'.format(
690 datetime=entry_time,
691 rev=entry['rev'],
692 status=entry['status'] + ('*%d' % entry['times']
693 if entry.get('times', 1) > 1 else ''),
694 values=entry.get('values', ''),
695 comment=entry.get('comment', '')))
696 else:
697 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800698
699 def cmd_next(self, _opts):
700 """Prints next suggested rev to bisect."""
701 self.strategy.rebuild()
702 if self.strategy.is_done():
703 print('done')
704 return
705
706 idx = self.strategy.next_idx()
707 rev = self.states.idx2rev(idx)
708 print(rev)
709
710 def cmd_switch(self, opts):
711 """Switches to given rev without eval."""
712 assert self.config.get('switch')
713
714 self.strategy.rebuild()
715
716 if opts.rev == 'next':
717 idx = self.strategy.next_idx()
718 rev = self.states.idx2rev(idx)
719 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800720 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800721 assert rev
722
723 logger.info('switch to %s', rev)
724 status = do_switch(self.config['switch'], self.domain, rev)
725 if status:
726 print('switch failed')
727
728 def _add_revs_status_helper(self, revs, status):
729 self.strategy.rebuild()
730 for rev, times in revs:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800731 self._add_sample(rev, status, times=times, comment='manual')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800732 self.states.save()
733
734 def cmd_new(self, opts):
735 """Tells bisect engine the said revs have "new" behavior."""
736 logger.info('set [%s] as new', opts.revs)
737 self._add_revs_status_helper(opts.revs, 'new')
738
739 def cmd_old(self, opts):
740 """Tells bisect engine the said revs have "old" behavior."""
741 logger.info('set [%s] as old', opts.revs)
742 self._add_revs_status_helper(opts.revs, 'old')
743
744 def cmd_skip(self, opts):
745 """Tells bisect engine the said revs have "skip" behavior."""
746 logger.info('set [%s] as skip', opts.revs)
747 self._add_revs_status_helper(opts.revs, 'skip')
748
749 def _create_states(self, session=None, session_base=None):
750 if not session:
751 session = DEFAULT_SESSION_NAME
752 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800753 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800754
755 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
756
757 if self.states:
758 assert self.states.session_file == session_file
759 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800760 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800761
762 def cmd_config(self, opts):
763 """Configures additional setting.
764
765 See config command's help message for more detail.
766 """
767 self.states.load()
768 self.domain = self.domain_cls(self.states.config)
769 if not opts.value:
770 print(self.states.config[opts.key])
771 return
772
773 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800774 result = check_executable(opts.value[0])
775 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800776 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800777
Kuang-che Wu88875db2017-07-20 10:47:53 +0800778 self.states.config[opts.key] = opts.value
779
780 elif opts.key == 'confidence':
781 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800782 raise errors.ArgumentError(
783 'confidence value',
784 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800785 try:
786 self.states.config[opts.key] = float(opts.value[0])
787 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800788 raise errors.ArgumentError('confidence value',
789 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800790
791 elif opts.key == 'noisy':
792 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800793 raise errors.ArgumentError(
794 'noisy value',
795 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800796 self.states.config[opts.key] = opts.value[0]
797
798 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800799 # unreachable
800 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800801
802 self.states.save()
803
804 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800805 if self.domain_cls.help:
806 description = self.domain_cls.help
807 else:
808 description = 'Bisector for %s' % self.domain_cls.__name__
809 description += textwrap.dedent('''
810 When running switcher and evaluator, it will set BISECT_REV environment
811 variable, indicates current rev to switch/evaluate.
812 ''')
813
Kuang-che Wu88875db2017-07-20 10:47:53 +0800814 parser = argparse.ArgumentParser(
815 prog=prog,
816 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800817 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800818 common.add_common_arguments(parser)
819 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800820 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800821 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800822 help='Directory to store sessions (default: %(default)r)')
823 parser.add_argument(
824 '--session',
825 default=DEFAULT_SESSION_NAME,
826 help='Session name (default: %(default)r)')
827 subparsers = parser.add_subparsers(
828 dest='command', title='commands', metavar='<command>')
829
830 parser_reset = subparsers.add_parser(
831 'reset', help='Reset bisect session and clean up saved result')
832 parser_reset.set_defaults(func=self.cmd_reset)
833
834 parser_init = subparsers.add_parser(
835 'init',
836 help='Initializes bisect session',
837 formatter_class=argparse.RawDescriptionHelpFormatter,
838 description=textwrap.dedent('''
839 Besides arguments for 'init' command, you also need to set 'switch'
840 and 'eval' command line via 'config' command.
841 $ bisector config switch <switch command and arguments>
842 $ bisector config eval <eval command and arguments>
843
844 The value of --noisy and --confidence could be changed by 'config'
845 command after 'init' as well.
846 '''))
847 parser_init.add_argument(
848 '--old',
849 required=True,
850 type=self.domain_cls.revtype,
851 help='Start of bisect range, which has old behavior')
852 parser_init.add_argument(
853 '--new',
854 required=True,
855 type=self.domain_cls.revtype,
856 help='End of bisect range, which has new behavior')
857 parser_init.add_argument(
858 '--noisy',
859 help='Enable noisy binary search and specify prior result. '
860 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
861 'and new fail rate increased to 2/3. '
862 'Skip if not flaky, say, "new=2/3" means old is always good.')
863 parser_init.add_argument(
Kuang-che Wu81cde452019-04-08 16:56:51 +0800864 '--old_value',
865 type=float,
866 help='For performance test, value of old behavior')
867 parser_init.add_argument(
868 '--new_value',
869 type=float,
870 help='For performance test, value of new behavior')
871 parser_init.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800872 '--confidence',
873 type=float,
874 default=DEFAULT_CONFIDENCE,
875 help='Confidence level (default: %(default)r)')
876 parser_init.set_defaults(func=self.cmd_init)
877 self.domain_cls.add_init_arguments(parser_init)
878
879 parser_config = subparsers.add_parser(
880 'config', help='Configures additional setting')
881 parser_config.add_argument(
882 'key',
883 choices=['switch', 'eval', 'confidence', 'noisy'],
884 metavar='key',
885 help='What config to change. choices=[%(choices)s]')
886 parser_config.add_argument(
887 'value', nargs=argparse.REMAINDER, help='New value')
888 parser_config.set_defaults(func=self.cmd_config)
889
890 parser_run = subparsers.add_parser(
891 'run',
892 help='Performs bisection',
893 formatter_class=argparse.RawDescriptionHelpFormatter,
894 description=textwrap.dedent('''
895 This command does switch and eval to determine candidates having old or
896 new behavior.
897
898 By default, it attempts to try versions in binary search manner until
899 found the first version having new behavior.
900
901 If version numbers are specified on command line, it just tries those
902 versions and record the result.
903
904 Example:
905 Bisect automatically.
906 $ %(prog)s
907
908 Switch and run version "2.13" and "2.14" and then stop.
909 $ %(prog)s 2.13 2.14
910 '''))
911 parser_run.add_argument(
912 '-1', '--once', action='store_true', help='Only run one step')
913 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800914 '--force',
915 action='store_true',
916 help="Run at least once even it's already done")
917 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800918 'revs',
919 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800920 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800921 help='revs to switch+eval; '
922 'default is calculating automatically and run until done')
923 parser_run.set_defaults(func=self.cmd_run)
924
925 parser_switch = subparsers.add_parser(
926 'switch', help='Switch to given rev without eval')
927 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800928 'rev',
929 type=argtype_multiplexer(self.domain_cls.intra_revtype,
930 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800931 parser_switch.set_defaults(func=self.cmd_switch)
932
933 parser_old = subparsers.add_parser(
934 'old', help='Tells bisect engine the said revs have "old" behavior')
935 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800936 'revs',
937 nargs='+',
938 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800939 parser_old.set_defaults(func=self.cmd_old)
940
941 parser_new = subparsers.add_parser(
942 'new', help='Tells bisect engine the said revs have "new" behavior')
943 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800944 'revs',
945 nargs='+',
946 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800947 parser_new.set_defaults(func=self.cmd_new)
948
949 parser_skip = subparsers.add_parser(
950 'skip', help='Tells bisect engine the said revs have "skip" behavior')
951 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800952 'revs',
953 nargs='+',
954 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800955 parser_skip.set_defaults(func=self.cmd_skip)
956
957 parser_view = subparsers.add_parser(
958 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800959 parser_view.add_argument('--verbose', '-v', action='store_true')
960 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800961 parser_view.set_defaults(func=self.cmd_view)
962
963 parser_log = subparsers.add_parser(
964 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800965 parser_log.add_argument('--before', type=float)
966 parser_log.add_argument('--after', type=float)
967 parser_log.add_argument(
968 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800969 parser_log.set_defaults(func=self.cmd_log)
970
971 parser_next = subparsers.add_parser(
972 'next', help='Prints next suggested rev to bisect')
973 parser_next.set_defaults(func=self.cmd_next)
974
975 return parser
976
977 def main(self, *args, **kwargs):
978 """Command line main function.
979
980 Args:
981 *args: Command line arguments.
982 **kwargs: additional non command line arguments passed by script code.
983 {
984 'prog': Program name; optional.
985 }
986 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800987 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800988 parser = self.create_argument_parser(kwargs.get('prog'))
989 opts = parser.parse_args(args or None)
990 common.config_logging(opts)
991
992 self._create_states(session=opts.session, session_base=opts.session_base)
993 if opts.command not in ('init', 'reset', 'config'):
994 self.states.load()
995 self.domain = self.domain_cls(self.states.config)
996 self.strategy = strategy.NoisyBinarySearch(
997 self.states.rev_info,
998 self.states.rev2idx(self.config['old']),
999 self.states.rev2idx(self.config['new']),
Kuang-che Wu81cde452019-04-08 16:56:51 +08001000 old_value=self.config['old_value'],
1001 new_value=self.config['new_value'],
Kuang-che Wu88875db2017-07-20 10:47:53 +08001002 confidence=self.config['confidence'],
1003 observation=self.config['noisy'])
1004
1005 return opts.func(opts)