blob: cf920a24da1a730f920c801a72c7c4894481e672 [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
30
31class ArgTypeError(argparse.ArgumentTypeError):
32 """An error for argument validation failure.
33
34 This not only tells users the argument is wrong but also gives correct
35 example. The main purpose of this error is for argtype_multiplexer, which
36 cascades examples from multiple ArgTypeError.
37 """
38
39 def __init__(self, msg, example):
40 self.msg = msg
41 if isinstance(example, list):
42 self.example = example
43 else:
44 self.example = [example]
45 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
46 super(ArgTypeError, self).__init__(full_msg)
47
48
49def argtype_notempty(s):
50 """Validates argument is not an empty string.
51
52 Args:
53 s: string to validate.
54
55 Raises:
56 ArgTypeError if argument is empty string.
57 """
58 if not s:
59 msg = 'should not be empty'
60 raise ArgTypeError(msg, 'foo')
61 return s
62
63
64def argtype_int(s):
65 """Validate argument is a number.
66
67 Args:
68 s: string to validate.
69
70 Raises:
71 ArgTypeError if argument is not a number.
72 """
73 try:
74 return str(int(s))
75 except ValueError:
76 raise ArgTypeError('should be a number', '123')
77
78
Kuang-che Wu603cdad2019-01-18 21:32:55 +080079def argtype_re(pattern, example):
80 r"""Validate argument matches `pattern`.
81
82 Args:
83 pattern: regex pattern
84 example: example string which matches `pattern`
85
86 Returns:
87 A new argtype function which matches regex `pattern`
88 """
89 assert re.match(pattern, example)
90
91 def validate(s):
92 if re.match(pattern, s):
93 return s
94 if re.escape(pattern) == pattern:
95 raise ArgTypeError('should be "%s"' % pattern, pattern)
96 raise ArgTypeError('should match "%s"' % pattern,
97 '"%s" like %s' % (pattern, example))
98
99 return validate
100
101
Kuang-che Wu88875db2017-07-20 10:47:53 +0800102def argtype_multiplexer(*args):
103 r"""argtype multiplexer
104
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800105 This function takes a list of argtypes and creates a new function matching
106 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800107
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800108 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800109 >>> argtype = argtype_multiplexer(argtype_int,
110 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800111 >>> argtype('123')
112 123
113 >>> argtype('r456')
114 r456
115 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800116 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800117
118 Args:
119 *args: list of argtypes or regex pattern.
120
121 Returns:
122 A new argtype function which matches *args.
123 """
124
125 def validate(s):
126 examples = []
127 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800128 try:
129 return t(s)
130 except ArgTypeError as e:
131 examples += e.example
132
133 msg = 'Invalid argument'
134 raise ArgTypeError(msg, examples)
135
136 return validate
137
138
139def argtype_multiplier(argtype):
140 """A new argtype that supports multiplier suffix of the given argtype.
141
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800142 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800143 Supports the given argtype accepting "foo" as argument, this function
144 generates a new argtype function which accepts argument like "foo*3".
145
146 Returns:
147 A new argtype function which returns (arg, times) where arg is accepted
148 by input `argtype` and times is repeating count. Note that if multiplier is
149 omitted, "times" is 1.
150 """
151
152 def helper(s):
153 m = re.match(r'^(.+)\*(\d+)$', s)
154 try:
155 if m:
156 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800157 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800158 except ArgTypeError as e:
159 # It should be okay to gives multiplier example only for the first one
160 # because it is just "example", no need to enumerate all possibilities.
161 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
162
163 return helper
164
165
166def argtype_dir_path(s):
167 """Validate argument is an existing directory.
168
169 Args:
170 s: string to validate.
171
172 Raises:
173 ArgTypeError if the path is not a directory.
174 """
175 if not os.path.exists(s):
176 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
177 if not os.path.isdir(s):
178 raise ArgTypeError('should be a directory', '/path/to/somewhere')
179
180 # Normalize, trim trailing path separators.
181 if len(s) > 1 and s[-1] == os.path.sep:
182 s = s[:-1]
183 return s
184
185
186def _collect_bisect_result_values(values, line):
187 """Collect bisect result values from output line.
188
189 Args:
190 values: Collected values are appending to this list.
191 line: One line of output string.
192 """
193 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
194 if m:
195 try:
196 values.extend(map(float, m.group(1).split()))
197 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800198 raise errors.InternalError(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800199 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
200
201
Kuang-che Wu88518882017-09-22 16:57:25 +0800202def check_executable(program):
203 """Checks whether a program is executable.
204
205 Args:
206 program: program path in question
207
208 Returns:
209 string as error message if `program` is not executable, or None otherwise.
210 It will return None if unable to determine as well.
211 """
212 returncode = util.call('which', program)
213 if returncode == 127: # No 'which' on this platform, skip the check.
214 return None
215 if returncode == 0: # is executable
216 return None
217
218 hint = ''
219 if not os.path.exists(program):
220 hint = 'Not in PATH?'
221 elif not os.path.isfile(program):
222 hint = 'Not a file'
223 elif not os.access(program, os.X_OK):
224 hint = 'Forgot to chmod +x?'
225 elif '/' not in program:
226 hint = 'Forgot to prepend "./" ?'
227 return '%r is not executable. %s' % (program, hint)
228
229
Kuang-che Wu443633f2019-02-27 00:58:33 +0800230def format_returncode(returncode):
231 if returncode < 0:
232 signum = -returncode
233 signame = 'Unknown'
234 for k, v in vars(signal).items():
235 if k.startswith('SIG') and '_' not in k and v == signum:
236 signame = k
237 return 'terminated by signal %d (%s)' % (signum, signame)
238
239 return 'exited with code %d' % returncode
240
241
Kuang-che Wu88875db2017-07-20 10:47:53 +0800242def do_evaluate(evaluate_cmd, domain, rev):
243 """Invokes evaluator command.
244
245 The `evaluate_cmd` can get the target revision from the environment variable
246 named 'BISECT_REV'.
247
248 The result is determined according to the exit code of evaluator:
249 0: 'old'
250 1..124: 'new'
251 125: 'skip'
252 126, 127: fatal error
253 terminated by signal: fatal error
254
255 p.s. the definition of result is compatible with git-bisect(1).
256
257 It also extracts additional values from evaluate_cmd's stdout lines which
258 match the following format:
259 BISECT_RESULT_VALUES=<float>[, <float>]*
260
261 Args:
262 evaluate_cmd: evaluator command.
263 domain: a bisect_kit.core.Domain instance.
264 rev: version to evaluate.
265
266 Returns:
267 (result, values):
268 result is one of 'old', 'new', 'skip'.
269 values are additional collected values, like performance score.
270
271 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800272 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800273 """
274 env = os.environ.copy()
275 env['BISECT_REV'] = rev
276 domain.setenv(env, rev)
277
278 values = []
279 p = util.Popen(
280 evaluate_cmd,
281 env=env,
282 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
283 returncode = p.wait()
284 if returncode < 0 or returncode > 125:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800285 raise errors.ExecutionFatalError(
286 'eval failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800287
288 if returncode == 0:
289 return 'old', values
290 if returncode == 125:
291 return 'skip', values
292 return 'new', values
293
294
295def do_switch(switch_cmd, domain, rev):
296 """Invokes switcher command.
297
298 The `switch_cmd` can get the target revision from the environment variable
299 named 'BISECT_REV'.
300
301 The result is determined according to the exit code of switcher:
302 0: switch succeeded
303 1..125: 'skip'
304 126, 127: fatal error
305 terminated by signal: fatal error
306
307 In other words, any non-fatal errors are considered as 'skip'.
308
309 Args:
310 switch_cmd: switcher command.
311 domain: a bisect_kit.core.Domain instance.
312 rev: version to switch.
313
314 Returns:
315 None if switch successfully, 'skip' otherwise.
316
317 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800318 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800319 """
320 env = os.environ.copy()
321 env['BISECT_REV'] = rev
322 domain.setenv(env, rev)
323
324 returncode = util.call(*switch_cmd, env=env)
325 if returncode < 0 or returncode > 125:
Kuang-che Wu443633f2019-02-27 00:58:33 +0800326 raise errors.ExecutionFatalError(
327 'switch failed: %s' % format_returncode(returncode))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800328
329 if returncode != 0:
330 return 'skip'
331 return None
332
333
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800334class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800335 """Bisector command line interface.
336
337 The typical usage pattern:
338
339 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800340 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800341
342 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800343 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800344
345 If you need to control the bisector using python code, the easier way is
346 passing command line arguments to main() function. For example,
347 bisector = Bisector(CustomDomain)
348 bisector.main('init', '--old', '123', '--new', '456')
349 bisector.main('config', 'switch', 'true')
350 bisector.main('config', 'eval', 'true')
351 bisector.main('run')
352 """
353
354 def __init__(self, domain_cls):
355 self.domain_cls = domain_cls
356 self.domain = None
357 self.states = None
358 self.strategy = None
359
360 @property
361 def config(self):
362 return self.states.config
363
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800364 def _format_status(self, status):
365 if status in ('old', 'new'):
Kuang-che Wub6756d42019-01-25 12:19:55 +0800366 return '%s behavior' % status
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800367 return status
368
Kuang-che Wu8b654092018-11-09 17:56:25 +0800369 def _add_sample(self, rev, status, **kwargs):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800370 idx = self.states.rev2idx(rev)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800371 self.states.add_sample(idx, status, **kwargs)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800372 self.strategy.update(idx, status)
373
374 def cmd_reset(self, _opts):
375 """Resets bisect session and clean up saved result."""
376 self.states.reset()
377
378 def cmd_init(self, opts):
379 """Initializes bisect session.
380
381 See init command's help message for more detail.
382 """
383 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800384 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800385 logger.debug('revlist %r', revlist)
386 if 'new' not in config:
387 config['new'] = opts.new
388 if 'old' not in config:
389 config['old'] = opts.old
390 assert len(revlist) >= 2
391 assert config['new'] in revlist
392 assert config['old'] in revlist
393 old_idx = revlist.index(config['old'])
394 new_idx = revlist.index(config['new'])
395 assert old_idx < new_idx
396
397 config.update(confidence=opts.confidence, noisy=opts.noisy)
398
399 self.states.init(config, revlist)
400 self.states.save()
401
402 def _switch_and_eval(self, rev, prev_rev=None):
403 """Switches and evaluates given version.
404
405 If current version equals to target, switch step will be skip.
406
407 Args:
408 rev: Target version.
409 prev_rev: Previous version.
410
411 Returns:
412 (step, status, values):
413 step: Last step executed ('switch' or 'eval').
414 status: Execution result ('old', 'new', or 'skip').
415 values: Collected values from eval step. None if last step is 'switch'.
416 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800417 if prev_rev != rev:
418 logger.debug('switch to rev=%s', rev)
419 t0 = time.time()
420 status = do_switch(self.config['switch'], self.domain, rev)
421 t1 = time.time()
422 if status == 'skip':
423 logger.debug('switch failed => skip')
424 return 'switch', status, None
425 self.states.data['stats']['switch_count'] += 1
426 self.states.data['stats']['switch_time'] += t1 - t0
427
428 logger.debug('eval rev=%s', rev)
429 t0 = time.time()
430 status, values = do_evaluate(self.config['eval'], self.domain, rev)
431 t1 = time.time()
432 if status == 'skip':
433 return 'eval', status, values
434 self.states.data['stats']['eval_count'] += 1
435 self.states.data['stats']['eval_time'] += t1 - t0
436
437 return 'eval', status, values
438
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800439 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800440 if opts.revs:
441 for rev in opts.revs:
442 idx = self.states.rev2idx(rev)
443 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
444 yield idx, rev
445 if opts.once:
446 break
447 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800448 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800449 idx = self.strategy.next_idx()
450 rev = self.states.idx2rev(idx)
451 logger.info('try idx=%d rev=%s', idx, rev)
452 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800453 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800454 if opts.once:
455 break
456
457 def cmd_run(self, opts):
458 """Performs bisection.
459
460 See run command's help message for more detail.
461
462 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800463 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800464 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800465 errors.UnableToProceed: Too many errors to narrow down further the
466 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800467 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800468 # Set dummy values in case exception raised before loop.
469 idx, rev = -1, None
470 try:
471 assert self.config.get('switch')
472 assert self.config.get('eval')
473 self.strategy.rebuild()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800474
Kuang-che Wu8b654092018-11-09 17:56:25 +0800475 prev_rev = None
476 force = opts.force
477 for idx, rev in self._next_idx_iter(opts, force):
478 if not force:
479 # Bail out if bisection range is unlikely true in order to prevent
480 # wasting time. This is necessary because some configurations (say,
481 # confidence) may be changed before cmd_run() and thus the bisection
482 # range becomes not acceptable.
483 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800484
Kuang-che Wu8b654092018-11-09 17:56:25 +0800485 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800486 logger.info('rev=%s status => %s', rev, self._format_status(status))
Kuang-che Wu8b654092018-11-09 17:56:25 +0800487 force = False
488
489 self._add_sample(rev, status, values=values)
490 self.states.save()
491
492 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800493 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800494
Kuang-che Wu8b654092018-11-09 17:56:25 +0800495 if status == 'skip':
496 current_state = self.states.get(idx)
497 if current_state['skip'] > (
498 current_state['old'] + current_state['new'] + 1) * 5:
499 message = 'too much "skip" for rev=%r' % rev
500 raise errors.UnableToProceed(message)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800501
Kuang-che Wu8b654092018-11-09 17:56:25 +0800502 self.strategy.show_summary()
503
504 if step == 'switch' and status == 'skip':
505 # Previous switch failed and thus the current version is unknown. Set
506 # it None, so next switch operation won't be bypassed (due to
507 # optimization).
508 prev_rev = None
509 else:
510 prev_rev = rev
511
512 logger.info('done')
513 old_idx, new_idx = self.strategy.get_range()
514 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800515 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800516 except Exception as e:
517 exception_name = e.__class__.__name__
518 self.states.add_history(
519 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
520 self.states.save()
521 raise
522 finally:
523 if rev:
524 # progress so far
525 old_idx, new_idx = self.strategy.get_range()
526 self.states.add_history(
527 'range',
528 old=self.states.idx2rev(old_idx),
529 new=self.states.idx2rev(new_idx))
530 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800531
532 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800533 """Shows remaining candidates."""
Kuang-che Wu15874b62019-01-11 21:10:27 +0800534 try:
535 self.strategy.rebuild()
536 # Rebuild twice in order to re-estimate noise.
537 self.strategy.rebuild()
538 except errors.VerificationFailed:
539 # Do nothing, go ahead to show existing information anyway.
540 pass
Kuang-che Wue80bb872018-11-15 19:45:25 +0800541
542 old_idx, new_idx = self.strategy.get_range()
543 old, new = map(self.states.idx2rev, [old_idx, new_idx])
544 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
545 self.strategy.confidence / 10.0)
546 summary = {
547 'rev_info': [vars(info).copy() for info in self.states.rev_info],
548 'current_range': (old, new),
549 'highlight_range':
550 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
551 'prob':
552 self.strategy.prob,
553 'remaining_steps':
554 self.strategy.remaining_steps(),
555 }
556
557 if opts.verbose or opts.json:
558 interesting_indexes = set(range(len(summary['rev_info'])))
559 else:
560 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800561 if self.strategy.prob:
562 for i, p in enumerate(self.strategy.prob):
563 if p > 0.05:
564 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800565
566 self.domain.fill_candidate_summary(summary, interesting_indexes)
567
568 if opts.json:
569 print(json.dumps(summary, indent=2, sort_keys=True))
570 else:
571 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
572
573 def show_summary(self, summary, interesting_indexes, verbose=False):
574 old, new = summary['current_range']
575 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
576
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800577 for link in summary.get('links', []):
578 print('%s: %s' % (link['name'], link['url']))
579 if 'note' in link:
580 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800581
582 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800583 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800584 print('(roughly %d steps)' % summary['remaining_steps'])
585
586 for i, rev_info in enumerate(summary['rev_info']):
587 if (not verbose and not old_idx <= i <= new_idx and
588 not rev_info['result_counter']):
589 continue
590
591 detail = []
Kuang-che Wu05e416e2019-02-21 12:33:52 +0800592 if self.strategy.is_noisy() and summary['prob']:
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800593 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800594 if rev_info['result_counter']:
595 detail.append(str(rev_info['result_counter']))
596 values = sorted(rev_info['values'])
597 if len(values) == 1:
598 detail.append('%.3f' % values[0])
599 elif len(values) > 1:
600 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
601 (len(values), sum(values) / len(values),
602 values[len(values) // 2], values[0], values[-1]))
603
604 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
605 if i in interesting_indexes:
606 if 'comment' in rev_info:
607 print('\t%s' % rev_info['comment'])
608 for action in rev_info.get('actions', []):
609 if 'text' in action:
610 print('\t%s' % action['text'])
611 if 'link' in action:
612 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800613
614 def current_status(self, session=None, session_base=None):
615 """Gets current bisect status.
616
617 Returns:
618 A dict describing current status. It contains following items:
619 inited: True iff the session file is initialized (init command has been
620 invoked). If not, below items are omitted.
621 old: Start of current estimated range.
622 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800623 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800624 estimated_noise: New estimated noise.
625 done: True if bisection is done, otherwise False.
626 """
627 self._create_states(session=session, session_base=session_base)
628 if self.states.load():
629 self.strategy = strategy.NoisyBinarySearch(
630 self.states.rev_info,
631 self.states.rev2idx(self.config['old']),
632 self.states.rev2idx(self.config['new']),
633 confidence=self.config['confidence'],
634 observation=self.config['noisy'])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800635 try:
636 self.strategy.rebuild()
637 except errors.VerificationFailed:
638 # Do nothing, go ahead to show existing information anyway.
639 pass
Kuang-che Wu88875db2017-07-20 10:47:53 +0800640 left, right = self.strategy.get_range()
641 estimated_noise = self.strategy.get_noise_observation()
642
643 result = dict(
644 inited=True,
645 old=self.states.idx2rev(left),
646 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800647 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800648 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800649 done=self.strategy.is_done())
650 else:
651 result = dict(inited=False)
652 return result
653
Kuang-che Wu8b654092018-11-09 17:56:25 +0800654 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800655 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800656 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800657 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800658 if opts.before and entry['timestamp'] >= opts.before:
659 continue
660 if opts.after and entry['timestamp'] <= opts.after:
661 continue
662 history.append(entry)
663
664 if opts.json:
665 print(json.dumps(history, indent=2))
666 return
667
668 for entry in history:
669 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
670 if entry.get('event', 'sample') == 'sample':
671 print('{datetime} {rev} {status} {values} {comment}'.format(
672 datetime=entry_time,
673 rev=entry['rev'],
674 status=entry['status'] + ('*%d' % entry['times']
675 if entry.get('times', 1) > 1 else ''),
676 values=entry.get('values', ''),
677 comment=entry.get('comment', '')))
678 else:
679 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800680
681 def cmd_next(self, _opts):
682 """Prints next suggested rev to bisect."""
683 self.strategy.rebuild()
684 if self.strategy.is_done():
685 print('done')
686 return
687
688 idx = self.strategy.next_idx()
689 rev = self.states.idx2rev(idx)
690 print(rev)
691
692 def cmd_switch(self, opts):
693 """Switches to given rev without eval."""
694 assert self.config.get('switch')
695
696 self.strategy.rebuild()
697
698 if opts.rev == 'next':
699 idx = self.strategy.next_idx()
700 rev = self.states.idx2rev(idx)
701 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800702 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800703 assert rev
704
705 logger.info('switch to %s', rev)
706 status = do_switch(self.config['switch'], self.domain, rev)
707 if status:
708 print('switch failed')
709
710 def _add_revs_status_helper(self, revs, status):
711 self.strategy.rebuild()
712 for rev, times in revs:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800713 self._add_sample(rev, status, times=times, comment='manual')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800714 self.states.save()
715
716 def cmd_new(self, opts):
717 """Tells bisect engine the said revs have "new" behavior."""
718 logger.info('set [%s] as new', opts.revs)
719 self._add_revs_status_helper(opts.revs, 'new')
720
721 def cmd_old(self, opts):
722 """Tells bisect engine the said revs have "old" behavior."""
723 logger.info('set [%s] as old', opts.revs)
724 self._add_revs_status_helper(opts.revs, 'old')
725
726 def cmd_skip(self, opts):
727 """Tells bisect engine the said revs have "skip" behavior."""
728 logger.info('set [%s] as skip', opts.revs)
729 self._add_revs_status_helper(opts.revs, 'skip')
730
731 def _create_states(self, session=None, session_base=None):
732 if not session:
733 session = DEFAULT_SESSION_NAME
734 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800735 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800736
737 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
738
739 if self.states:
740 assert self.states.session_file == session_file
741 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800742 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800743
744 def cmd_config(self, opts):
745 """Configures additional setting.
746
747 See config command's help message for more detail.
748 """
749 self.states.load()
750 self.domain = self.domain_cls(self.states.config)
751 if not opts.value:
752 print(self.states.config[opts.key])
753 return
754
755 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800756 result = check_executable(opts.value[0])
757 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800758 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800759
Kuang-che Wu88875db2017-07-20 10:47:53 +0800760 self.states.config[opts.key] = opts.value
761
762 elif opts.key == 'confidence':
763 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800764 raise errors.ArgumentError(
765 'confidence value',
766 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800767 try:
768 self.states.config[opts.key] = float(opts.value[0])
769 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800770 raise errors.ArgumentError('confidence value',
771 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800772
773 elif opts.key == 'noisy':
774 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800775 raise errors.ArgumentError(
776 'noisy value',
777 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800778 self.states.config[opts.key] = opts.value[0]
779
780 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800781 # unreachable
782 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800783
784 self.states.save()
785
786 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800787 if self.domain_cls.help:
788 description = self.domain_cls.help
789 else:
790 description = 'Bisector for %s' % self.domain_cls.__name__
791 description += textwrap.dedent('''
792 When running switcher and evaluator, it will set BISECT_REV environment
793 variable, indicates current rev to switch/evaluate.
794 ''')
795
Kuang-che Wu88875db2017-07-20 10:47:53 +0800796 parser = argparse.ArgumentParser(
797 prog=prog,
798 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800799 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800800 common.add_common_arguments(parser)
801 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800802 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800803 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800804 help='Directory to store sessions (default: %(default)r)')
805 parser.add_argument(
806 '--session',
807 default=DEFAULT_SESSION_NAME,
808 help='Session name (default: %(default)r)')
809 subparsers = parser.add_subparsers(
810 dest='command', title='commands', metavar='<command>')
811
812 parser_reset = subparsers.add_parser(
813 'reset', help='Reset bisect session and clean up saved result')
814 parser_reset.set_defaults(func=self.cmd_reset)
815
816 parser_init = subparsers.add_parser(
817 'init',
818 help='Initializes bisect session',
819 formatter_class=argparse.RawDescriptionHelpFormatter,
820 description=textwrap.dedent('''
821 Besides arguments for 'init' command, you also need to set 'switch'
822 and 'eval' command line via 'config' command.
823 $ bisector config switch <switch command and arguments>
824 $ bisector config eval <eval command and arguments>
825
826 The value of --noisy and --confidence could be changed by 'config'
827 command after 'init' as well.
828 '''))
829 parser_init.add_argument(
830 '--old',
831 required=True,
832 type=self.domain_cls.revtype,
833 help='Start of bisect range, which has old behavior')
834 parser_init.add_argument(
835 '--new',
836 required=True,
837 type=self.domain_cls.revtype,
838 help='End of bisect range, which has new behavior')
839 parser_init.add_argument(
840 '--noisy',
841 help='Enable noisy binary search and specify prior result. '
842 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
843 'and new fail rate increased to 2/3. '
844 'Skip if not flaky, say, "new=2/3" means old is always good.')
845 parser_init.add_argument(
846 '--confidence',
847 type=float,
848 default=DEFAULT_CONFIDENCE,
849 help='Confidence level (default: %(default)r)')
850 parser_init.set_defaults(func=self.cmd_init)
851 self.domain_cls.add_init_arguments(parser_init)
852
853 parser_config = subparsers.add_parser(
854 'config', help='Configures additional setting')
855 parser_config.add_argument(
856 'key',
857 choices=['switch', 'eval', 'confidence', 'noisy'],
858 metavar='key',
859 help='What config to change. choices=[%(choices)s]')
860 parser_config.add_argument(
861 'value', nargs=argparse.REMAINDER, help='New value')
862 parser_config.set_defaults(func=self.cmd_config)
863
864 parser_run = subparsers.add_parser(
865 'run',
866 help='Performs bisection',
867 formatter_class=argparse.RawDescriptionHelpFormatter,
868 description=textwrap.dedent('''
869 This command does switch and eval to determine candidates having old or
870 new behavior.
871
872 By default, it attempts to try versions in binary search manner until
873 found the first version having new behavior.
874
875 If version numbers are specified on command line, it just tries those
876 versions and record the result.
877
878 Example:
879 Bisect automatically.
880 $ %(prog)s
881
882 Switch and run version "2.13" and "2.14" and then stop.
883 $ %(prog)s 2.13 2.14
884 '''))
885 parser_run.add_argument(
886 '-1', '--once', action='store_true', help='Only run one step')
887 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800888 '--force',
889 action='store_true',
890 help="Run at least once even it's already done")
891 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800892 'revs',
893 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800894 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800895 help='revs to switch+eval; '
896 'default is calculating automatically and run until done')
897 parser_run.set_defaults(func=self.cmd_run)
898
899 parser_switch = subparsers.add_parser(
900 'switch', help='Switch to given rev without eval')
901 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800902 'rev',
903 type=argtype_multiplexer(self.domain_cls.intra_revtype,
904 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800905 parser_switch.set_defaults(func=self.cmd_switch)
906
907 parser_old = subparsers.add_parser(
908 'old', help='Tells bisect engine the said revs have "old" behavior')
909 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800910 'revs',
911 nargs='+',
912 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800913 parser_old.set_defaults(func=self.cmd_old)
914
915 parser_new = subparsers.add_parser(
916 'new', help='Tells bisect engine the said revs have "new" behavior')
917 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800918 'revs',
919 nargs='+',
920 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800921 parser_new.set_defaults(func=self.cmd_new)
922
923 parser_skip = subparsers.add_parser(
924 'skip', help='Tells bisect engine the said revs have "skip" behavior')
925 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800926 'revs',
927 nargs='+',
928 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800929 parser_skip.set_defaults(func=self.cmd_skip)
930
931 parser_view = subparsers.add_parser(
932 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800933 parser_view.add_argument('--verbose', '-v', action='store_true')
934 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800935 parser_view.set_defaults(func=self.cmd_view)
936
937 parser_log = subparsers.add_parser(
938 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800939 parser_log.add_argument('--before', type=float)
940 parser_log.add_argument('--after', type=float)
941 parser_log.add_argument(
942 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800943 parser_log.set_defaults(func=self.cmd_log)
944
945 parser_next = subparsers.add_parser(
946 'next', help='Prints next suggested rev to bisect')
947 parser_next.set_defaults(func=self.cmd_next)
948
949 return parser
950
951 def main(self, *args, **kwargs):
952 """Command line main function.
953
954 Args:
955 *args: Command line arguments.
956 **kwargs: additional non command line arguments passed by script code.
957 {
958 'prog': Program name; optional.
959 }
960 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800961 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800962 parser = self.create_argument_parser(kwargs.get('prog'))
963 opts = parser.parse_args(args or None)
964 common.config_logging(opts)
965
966 self._create_states(session=opts.session, session_base=opts.session_base)
967 if opts.command not in ('init', 'reset', 'config'):
968 self.states.load()
969 self.domain = self.domain_cls(self.states.config)
970 self.strategy = strategy.NoisyBinarySearch(
971 self.states.rev_info,
972 self.states.rev2idx(self.config['old']),
973 self.states.rev2idx(self.config['new']),
974 confidence=self.config['confidence'],
975 observation=self.config['noisy'])
976
977 return opts.func(opts)