blob: 9515111f2bb62878cafb2db9a228574abffad636 [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
10import logging
11import os
12import re
13import sys
14import textwrap
15import time
16
17from bisect_kit import common
Kuang-che Wu385279d2017-09-27 14:48:28 +080018from bisect_kit import configure
Kuang-che Wu88875db2017-07-20 10:47:53 +080019from bisect_kit import core
20from bisect_kit import strategy
21from bisect_kit import util
22
23logger = logging.getLogger(__name__)
24
25DEFAULT_SESSION_BASE = 'bisect.sessions'
26DEFAULT_SESSION_NAME = 'default'
27DEFAULT_CONFIDENCE = 0.999
28
29
30class ArgTypeError(argparse.ArgumentTypeError):
31 """An error for argument validation failure.
32
33 This not only tells users the argument is wrong but also gives correct
34 example. The main purpose of this error is for argtype_multiplexer, which
35 cascades examples from multiple ArgTypeError.
36 """
37
38 def __init__(self, msg, example):
39 self.msg = msg
40 if isinstance(example, list):
41 self.example = example
42 else:
43 self.example = [example]
44 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
45 super(ArgTypeError, self).__init__(full_msg)
46
47
48def argtype_notempty(s):
49 """Validates argument is not an empty string.
50
51 Args:
52 s: string to validate.
53
54 Raises:
55 ArgTypeError if argument is empty string.
56 """
57 if not s:
58 msg = 'should not be empty'
59 raise ArgTypeError(msg, 'foo')
60 return s
61
62
63def argtype_int(s):
64 """Validate argument is a number.
65
66 Args:
67 s: string to validate.
68
69 Raises:
70 ArgTypeError if argument is not a number.
71 """
72 try:
73 return str(int(s))
74 except ValueError:
75 raise ArgTypeError('should be a number', '123')
76
77
78def argtype_multiplexer(*args):
79 r"""argtype multiplexer
80
81 This function takes a list of argtypes or regex patterns and creates a new
82 function matching them. Moreover, it gives error message with examples.
83
Kuang-che Wubaaa4532018-08-15 17:08:10 +080084 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +080085 >>> argtype = argtype_multiplexer(argtype_int, r'^r\d+$')
86 >>> argtype('123')
87 123
88 >>> argtype('r456')
89 r456
90 >>> argtype('hello')
91 ArgTypeError: Invalid argument (example value: 123, r\d+$)
92
93 Args:
94 *args: list of argtypes or regex pattern.
95
96 Returns:
97 A new argtype function which matches *args.
98 """
99
100 def validate(s):
101 examples = []
102 for t in args:
103 if isinstance(t, str):
104 if re.match(t, s):
105 return s
106 examples.append(t)
107 continue
108 try:
109 return t(s)
110 except ArgTypeError as e:
111 examples += e.example
112
113 msg = 'Invalid argument'
114 raise ArgTypeError(msg, examples)
115
116 return validate
117
118
119def argtype_multiplier(argtype):
120 """A new argtype that supports multiplier suffix of the given argtype.
121
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800122 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800123 Supports the given argtype accepting "foo" as argument, this function
124 generates a new argtype function which accepts argument like "foo*3".
125
126 Returns:
127 A new argtype function which returns (arg, times) where arg is accepted
128 by input `argtype` and times is repeating count. Note that if multiplier is
129 omitted, "times" is 1.
130 """
131
132 def helper(s):
133 m = re.match(r'^(.+)\*(\d+)$', s)
134 try:
135 if m:
136 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800137 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800138 except ArgTypeError as e:
139 # It should be okay to gives multiplier example only for the first one
140 # because it is just "example", no need to enumerate all possibilities.
141 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
142
143 return helper
144
145
146def argtype_dir_path(s):
147 """Validate argument is an existing directory.
148
149 Args:
150 s: string to validate.
151
152 Raises:
153 ArgTypeError if the path is not a directory.
154 """
155 if not os.path.exists(s):
156 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
157 if not os.path.isdir(s):
158 raise ArgTypeError('should be a directory', '/path/to/somewhere')
159
160 # Normalize, trim trailing path separators.
161 if len(s) > 1 and s[-1] == os.path.sep:
162 s = s[:-1]
163 return s
164
165
166def _collect_bisect_result_values(values, line):
167 """Collect bisect result values from output line.
168
169 Args:
170 values: Collected values are appending to this list.
171 line: One line of output string.
172 """
173 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
174 if m:
175 try:
176 values.extend(map(float, m.group(1).split()))
177 except ValueError:
178 raise core.ExecutionFatalError(
179 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
180
181
Kuang-che Wu88518882017-09-22 16:57:25 +0800182def check_executable(program):
183 """Checks whether a program is executable.
184
185 Args:
186 program: program path in question
187
188 Returns:
189 string as error message if `program` is not executable, or None otherwise.
190 It will return None if unable to determine as well.
191 """
192 returncode = util.call('which', program)
193 if returncode == 127: # No 'which' on this platform, skip the check.
194 return None
195 if returncode == 0: # is executable
196 return None
197
198 hint = ''
199 if not os.path.exists(program):
200 hint = 'Not in PATH?'
201 elif not os.path.isfile(program):
202 hint = 'Not a file'
203 elif not os.access(program, os.X_OK):
204 hint = 'Forgot to chmod +x?'
205 elif '/' not in program:
206 hint = 'Forgot to prepend "./" ?'
207 return '%r is not executable. %s' % (program, hint)
208
209
Kuang-che Wu88875db2017-07-20 10:47:53 +0800210def do_evaluate(evaluate_cmd, domain, rev):
211 """Invokes evaluator command.
212
213 The `evaluate_cmd` can get the target revision from the environment variable
214 named 'BISECT_REV'.
215
216 The result is determined according to the exit code of evaluator:
217 0: 'old'
218 1..124: 'new'
219 125: 'skip'
220 126, 127: fatal error
221 terminated by signal: fatal error
222
223 p.s. the definition of result is compatible with git-bisect(1).
224
225 It also extracts additional values from evaluate_cmd's stdout lines which
226 match the following format:
227 BISECT_RESULT_VALUES=<float>[, <float>]*
228
229 Args:
230 evaluate_cmd: evaluator command.
231 domain: a bisect_kit.core.Domain instance.
232 rev: version to evaluate.
233
234 Returns:
235 (result, values):
236 result is one of 'old', 'new', 'skip'.
237 values are additional collected values, like performance score.
238
239 Raises:
240 core.ExecutionFatalError if evaluator returned fatal error code.
241 """
242 env = os.environ.copy()
243 env['BISECT_REV'] = rev
244 domain.setenv(env, rev)
245
246 values = []
247 p = util.Popen(
248 evaluate_cmd,
249 env=env,
250 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
251 returncode = p.wait()
252 if returncode < 0 or returncode > 125:
253 raise core.ExecutionFatalError(str(returncode))
254
255 if returncode == 0:
256 return 'old', values
257 if returncode == 125:
258 return 'skip', values
259 return 'new', values
260
261
262def do_switch(switch_cmd, domain, rev):
263 """Invokes switcher command.
264
265 The `switch_cmd` can get the target revision from the environment variable
266 named 'BISECT_REV'.
267
268 The result is determined according to the exit code of switcher:
269 0: switch succeeded
270 1..125: 'skip'
271 126, 127: fatal error
272 terminated by signal: fatal error
273
274 In other words, any non-fatal errors are considered as 'skip'.
275
276 Args:
277 switch_cmd: switcher command.
278 domain: a bisect_kit.core.Domain instance.
279 rev: version to switch.
280
281 Returns:
282 None if switch successfully, 'skip' otherwise.
283
284 Raises:
285 core.ExecutionFatalError if switcher returned fatal error code.
286 """
287 env = os.environ.copy()
288 env['BISECT_REV'] = rev
289 domain.setenv(env, rev)
290
291 returncode = util.call(*switch_cmd, env=env)
292 if returncode < 0 or returncode > 125:
293 raise core.ExecutionFatalError(str(returncode))
294
295 if returncode != 0:
296 return 'skip'
297 return None
298
299
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800300class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800301 """Bisector command line interface.
302
303 The typical usage pattern:
304
305 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800306 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800307
308 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800309 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800310
311 If you need to control the bisector using python code, the easier way is
312 passing command line arguments to main() function. For example,
313 bisector = Bisector(CustomDomain)
314 bisector.main('init', '--old', '123', '--new', '456')
315 bisector.main('config', 'switch', 'true')
316 bisector.main('config', 'eval', 'true')
317 bisector.main('run')
318 """
319
320 def __init__(self, domain_cls):
321 self.domain_cls = domain_cls
322 self.domain = None
323 self.states = None
324 self.strategy = None
325
326 @property
327 def config(self):
328 return self.states.config
329
330 def _add_status(self, rev, status, **kwargs):
331 idx = self.states.rev2idx(rev)
332 self.states.add_status(idx, status, **kwargs)
333 self.strategy.update(idx, status)
334
335 def cmd_reset(self, _opts):
336 """Resets bisect session and clean up saved result."""
337 self.states.reset()
338
339 def cmd_init(self, opts):
340 """Initializes bisect session.
341
342 See init command's help message for more detail.
343 """
344 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800345 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800346 logger.debug('revlist %r', revlist)
347 if 'new' not in config:
348 config['new'] = opts.new
349 if 'old' not in config:
350 config['old'] = opts.old
351 assert len(revlist) >= 2
352 assert config['new'] in revlist
353 assert config['old'] in revlist
354 old_idx = revlist.index(config['old'])
355 new_idx = revlist.index(config['new'])
356 assert old_idx < new_idx
357
358 config.update(confidence=opts.confidence, noisy=opts.noisy)
359
360 self.states.init(config, revlist)
361 self.states.save()
362
363 def _switch_and_eval(self, rev, prev_rev=None):
364 """Switches and evaluates given version.
365
366 If current version equals to target, switch step will be skip.
367
368 Args:
369 rev: Target version.
370 prev_rev: Previous version.
371
372 Returns:
373 (step, status, values):
374 step: Last step executed ('switch' or 'eval').
375 status: Execution result ('old', 'new', or 'skip').
376 values: Collected values from eval step. None if last step is 'switch'.
377 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800378 if prev_rev != rev:
379 logger.debug('switch to rev=%s', rev)
380 t0 = time.time()
381 status = do_switch(self.config['switch'], self.domain, rev)
382 t1 = time.time()
383 if status == 'skip':
384 logger.debug('switch failed => skip')
385 return 'switch', status, None
386 self.states.data['stats']['switch_count'] += 1
387 self.states.data['stats']['switch_time'] += t1 - t0
388
389 logger.debug('eval rev=%s', rev)
390 t0 = time.time()
391 status, values = do_evaluate(self.config['eval'], self.domain, rev)
392 t1 = time.time()
393 if status == 'skip':
394 return 'eval', status, values
395 self.states.data['stats']['eval_count'] += 1
396 self.states.data['stats']['eval_time'] += t1 - t0
397
398 return 'eval', status, values
399
400 def _next_idx_iter(self, opts):
401 if opts.revs:
402 for rev in opts.revs:
403 idx = self.states.rev2idx(rev)
404 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
405 yield idx, rev
406 if opts.once:
407 break
408 else:
409 while not self.strategy.is_done():
410 idx = self.strategy.next_idx()
411 rev = self.states.idx2rev(idx)
412 logger.info('try idx=%d rev=%s', idx, rev)
413 yield idx, rev
414 if opts.once:
415 break
416
417 def cmd_run(self, opts):
418 """Performs bisection.
419
420 See run command's help message for more detail.
421
422 Raises:
423 core.VerificationFailed: The bisection range cannot be verified. We
424 expect 'old' at the first rev and 'new' at last rev.
425 core.ExecutionFatalError: Fatal error, bisector stop.
426 strategy.WrongAssumption: Eval results contradicted.
427 """
428 assert self.config.get('switch')
429 assert self.config.get('eval')
430
431 self.strategy.rebuild()
432
433 prev_rev = None
434 for idx, rev in self._next_idx_iter(opts):
435 # Bail out if bisection range is unlikely true in order to prevent
436 # wasting time. This is necessary because some configurations (say,
437 # confidence) may be changed before cmd_run() and thus the bisection
438 # range becomes not acceptable.
439 self.strategy.check_verification_range()
440
441 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
442 logger.info('rev=%s => status %s', rev, status)
443
444 self._add_status(rev, status, values=values)
445 # Bail out if bisection range is unlikely true. Don't save.
446 # The last failing results are likely something wrong in bisection setup,
447 # bisector, and/or evaluator. The benefits of not saving the last failing
448 # results, are that users can just resume bisector directly without needs
449 # of erasing bad results after they fixed the problems.
450 self.strategy.check_verification_range()
451
452 if status == 'skip':
453 current_state = self.states.get(idx)
454 # Bail out if 'skip' too much times. Don't save.
455 if current_state['skip'] > (
456 current_state['old'] + current_state['new'] + 1) * 5:
457 raise core.ExecutionFatalError('too much "skip" for rev=%r' % rev)
458
459 self.states.save()
460
461 self.strategy.show_summary()
462
463 if step == 'switch' and status == 'skip':
464 # Previous switch failed and thus the current version is unknown. Set
465 # it None, so next switch operation won't be bypassed (due to
466 # optimization).
467 prev_rev = None
468 else:
469 prev_rev = rev
470
471 logger.info('done')
472
473 def cmd_view(self, opts):
474 """Shows current progress and candidates."""
475 self.strategy.rebuild()
476 # Rebuild twice in order to re-estimate noise.
477 self.strategy.rebuild()
478 self.strategy.show_summary(more=opts.more)
479 left, right = self.strategy.get_range()
480 self.domain.view(self.states.idx2rev(left), self.states.idx2rev(right))
481
482 def current_status(self, session=None, session_base=None):
483 """Gets current bisect status.
484
485 Returns:
486 A dict describing current status. It contains following items:
487 inited: True iff the session file is initialized (init command has been
488 invoked). If not, below items are omitted.
489 old: Start of current estimated range.
490 new: End of current estimated range.
491 estimated_noise: New estimated noise.
492 done: True if bisection is done, otherwise False.
493 """
494 self._create_states(session=session, session_base=session_base)
495 if self.states.load():
496 self.strategy = strategy.NoisyBinarySearch(
497 self.states.rev_info,
498 self.states.rev2idx(self.config['old']),
499 self.states.rev2idx(self.config['new']),
500 confidence=self.config['confidence'],
501 observation=self.config['noisy'])
502 self.strategy.rebuild()
503 left, right = self.strategy.get_range()
504 estimated_noise = self.strategy.get_noise_observation()
505
506 result = dict(
507 inited=True,
508 old=self.states.idx2rev(left),
509 new=self.states.idx2rev(right),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800510 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800511 done=self.strategy.is_done())
512 else:
513 result = dict(inited=False)
514 return result
515
516 def cmd_log(self, _opts):
517 """Prints what has been done so far."""
518 for entry in self.states.data['history']:
519 print('{datetime}, {rev} {status_time} {values} {comment_display}'.format(
520 datetime=datetime.datetime.fromtimestamp(entry['time']),
521 status_time=entry['status'] + ('*%d' % entry['times']
522 if entry['times'] > 1 else ''),
523 comment_display=entry['comment'] or '',
524 **entry))
525
526 def cmd_next(self, _opts):
527 """Prints next suggested rev to bisect."""
528 self.strategy.rebuild()
529 if self.strategy.is_done():
530 print('done')
531 return
532
533 idx = self.strategy.next_idx()
534 rev = self.states.idx2rev(idx)
535 print(rev)
536
537 def cmd_switch(self, opts):
538 """Switches to given rev without eval."""
539 assert self.config.get('switch')
540
541 self.strategy.rebuild()
542
543 if opts.rev == 'next':
544 idx = self.strategy.next_idx()
545 rev = self.states.idx2rev(idx)
546 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800547 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800548 assert rev
549
550 logger.info('switch to %s', rev)
551 status = do_switch(self.config['switch'], self.domain, rev)
552 if status:
553 print('switch failed')
554
555 def _add_revs_status_helper(self, revs, status):
556 self.strategy.rebuild()
557 for rev, times in revs:
558 self._add_status(rev, status, times=times, comment='manual')
559 self.states.save()
560
561 def cmd_new(self, opts):
562 """Tells bisect engine the said revs have "new" behavior."""
563 logger.info('set [%s] as new', opts.revs)
564 self._add_revs_status_helper(opts.revs, 'new')
565
566 def cmd_old(self, opts):
567 """Tells bisect engine the said revs have "old" behavior."""
568 logger.info('set [%s] as old', opts.revs)
569 self._add_revs_status_helper(opts.revs, 'old')
570
571 def cmd_skip(self, opts):
572 """Tells bisect engine the said revs have "skip" behavior."""
573 logger.info('set [%s] as skip', opts.revs)
574 self._add_revs_status_helper(opts.revs, 'skip')
575
576 def _create_states(self, session=None, session_base=None):
577 if not session:
578 session = DEFAULT_SESSION_NAME
579 if not session_base:
Kuang-che Wu385279d2017-09-27 14:48:28 +0800580 session_base = configure.get('SESSION_BASE', DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800581
582 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
583
584 if self.states:
585 assert self.states.session_file == session_file
586 else:
587 self.states = core.States(session_file)
588
589 def cmd_config(self, opts):
590 """Configures additional setting.
591
592 See config command's help message for more detail.
593 """
594 self.states.load()
595 self.domain = self.domain_cls(self.states.config)
596 if not opts.value:
597 print(self.states.config[opts.key])
598 return
599
600 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800601 result = check_executable(opts.value[0])
602 if result:
603 raise core.ExecutionFatalError('%s config %s: %s' % (sys.argv[0],
604 opts.key, result))
605
Kuang-che Wu88875db2017-07-20 10:47:53 +0800606 self.states.config[opts.key] = opts.value
607
608 elif opts.key == 'confidence':
609 if len(opts.value) != 1:
610 raise core.ExecutionFatalError(
611 '%s config %s: expected 1 value, %d values given' %
612 (sys.argv[0], opts.key, len(opts.value)))
613 try:
614 self.states.config[opts.key] = float(opts.value[0])
615 except ValueError:
616 raise core.ExecutionFatalError('%s config %s: invalid float value: %r' %
617 (sys.argv[0], opts.key, opts.value[0]))
618
619 elif opts.key == 'noisy':
620 if len(opts.value) != 1:
621 raise core.ExecutionFatalError(
622 '%s config %s: expected 1 value, %d values given' %
623 (sys.argv[0], opts.key, len(opts.value)))
624 self.states.config[opts.key] = opts.value[0]
625
626 else:
627 raise core.ExecutionFatalError('%s config: unknown key: %r' %
628 (sys.argv[0], opts.key))
629
630 self.states.save()
631
632 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800633 if self.domain_cls.help:
634 description = self.domain_cls.help
635 else:
636 description = 'Bisector for %s' % self.domain_cls.__name__
637 description += textwrap.dedent('''
638 When running switcher and evaluator, it will set BISECT_REV environment
639 variable, indicates current rev to switch/evaluate.
640 ''')
641
Kuang-che Wu88875db2017-07-20 10:47:53 +0800642 parser = argparse.ArgumentParser(
643 prog=prog,
644 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800645 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800646 common.add_common_arguments(parser)
647 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800648 '--session_base',
Kuang-che Wu385279d2017-09-27 14:48:28 +0800649 default=configure.get('SESSION_BASE', DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800650 help='Directory to store sessions (default: %(default)r)')
651 parser.add_argument(
652 '--session',
653 default=DEFAULT_SESSION_NAME,
654 help='Session name (default: %(default)r)')
655 subparsers = parser.add_subparsers(
656 dest='command', title='commands', metavar='<command>')
657
658 parser_reset = subparsers.add_parser(
659 'reset', help='Reset bisect session and clean up saved result')
660 parser_reset.set_defaults(func=self.cmd_reset)
661
662 parser_init = subparsers.add_parser(
663 'init',
664 help='Initializes bisect session',
665 formatter_class=argparse.RawDescriptionHelpFormatter,
666 description=textwrap.dedent('''
667 Besides arguments for 'init' command, you also need to set 'switch'
668 and 'eval' command line via 'config' command.
669 $ bisector config switch <switch command and arguments>
670 $ bisector config eval <eval command and arguments>
671
672 The value of --noisy and --confidence could be changed by 'config'
673 command after 'init' as well.
674 '''))
675 parser_init.add_argument(
676 '--old',
677 required=True,
678 type=self.domain_cls.revtype,
679 help='Start of bisect range, which has old behavior')
680 parser_init.add_argument(
681 '--new',
682 required=True,
683 type=self.domain_cls.revtype,
684 help='End of bisect range, which has new behavior')
685 parser_init.add_argument(
686 '--noisy',
687 help='Enable noisy binary search and specify prior result. '
688 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
689 'and new fail rate increased to 2/3. '
690 'Skip if not flaky, say, "new=2/3" means old is always good.')
691 parser_init.add_argument(
692 '--confidence',
693 type=float,
694 default=DEFAULT_CONFIDENCE,
695 help='Confidence level (default: %(default)r)')
696 parser_init.set_defaults(func=self.cmd_init)
697 self.domain_cls.add_init_arguments(parser_init)
698
699 parser_config = subparsers.add_parser(
700 'config', help='Configures additional setting')
701 parser_config.add_argument(
702 'key',
703 choices=['switch', 'eval', 'confidence', 'noisy'],
704 metavar='key',
705 help='What config to change. choices=[%(choices)s]')
706 parser_config.add_argument(
707 'value', nargs=argparse.REMAINDER, help='New value')
708 parser_config.set_defaults(func=self.cmd_config)
709
710 parser_run = subparsers.add_parser(
711 'run',
712 help='Performs bisection',
713 formatter_class=argparse.RawDescriptionHelpFormatter,
714 description=textwrap.dedent('''
715 This command does switch and eval to determine candidates having old or
716 new behavior.
717
718 By default, it attempts to try versions in binary search manner until
719 found the first version having new behavior.
720
721 If version numbers are specified on command line, it just tries those
722 versions and record the result.
723
724 Example:
725 Bisect automatically.
726 $ %(prog)s
727
728 Switch and run version "2.13" and "2.14" and then stop.
729 $ %(prog)s 2.13 2.14
730 '''))
731 parser_run.add_argument(
732 '-1', '--once', action='store_true', help='Only run one step')
733 parser_run.add_argument(
734 'revs',
735 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800736 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800737 help='revs to switch+eval; '
738 'default is calculating automatically and run until done')
739 parser_run.set_defaults(func=self.cmd_run)
740
741 parser_switch = subparsers.add_parser(
742 'switch', help='Switch to given rev without eval')
743 parser_switch.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800744 'rev', type=argtype_multiplexer(self.domain_cls.intra_revtype, 'next'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800745 parser_switch.set_defaults(func=self.cmd_switch)
746
747 parser_old = subparsers.add_parser(
748 'old', help='Tells bisect engine the said revs have "old" behavior')
749 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800750 'revs',
751 nargs='+',
752 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800753 parser_old.set_defaults(func=self.cmd_old)
754
755 parser_new = subparsers.add_parser(
756 'new', help='Tells bisect engine the said revs have "new" behavior')
757 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800758 'revs',
759 nargs='+',
760 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800761 parser_new.set_defaults(func=self.cmd_new)
762
763 parser_skip = subparsers.add_parser(
764 'skip', help='Tells bisect engine the said revs have "skip" behavior')
765 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800766 'revs',
767 nargs='+',
768 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800769 parser_skip.set_defaults(func=self.cmd_skip)
770
771 parser_view = subparsers.add_parser(
772 'view', help='Shows current progress and candidates')
773 parser_view.add_argument('--more', action='store_true')
774 parser_view.set_defaults(func=self.cmd_view)
775
776 parser_log = subparsers.add_parser(
777 'log', help='Prints what has been done so far')
778 parser_log.set_defaults(func=self.cmd_log)
779
780 parser_next = subparsers.add_parser(
781 'next', help='Prints next suggested rev to bisect')
782 parser_next.set_defaults(func=self.cmd_next)
783
784 return parser
785
786 def main(self, *args, **kwargs):
787 """Command line main function.
788
789 Args:
790 *args: Command line arguments.
791 **kwargs: additional non command line arguments passed by script code.
792 {
793 'prog': Program name; optional.
794 }
795 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800796 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800797 parser = self.create_argument_parser(kwargs.get('prog'))
798 opts = parser.parse_args(args or None)
799 common.config_logging(opts)
800
801 self._create_states(session=opts.session, session_base=opts.session_base)
802 if opts.command not in ('init', 'reset', 'config'):
803 self.states.load()
804 self.domain = self.domain_cls(self.states.config)
805 self.strategy = strategy.NoisyBinarySearch(
806 self.states.rev_info,
807 self.states.rev2idx(self.config['old']),
808 self.states.rev2idx(self.config['new']),
809 confidence=self.config['confidence'],
810 observation=self.config['noisy'])
811
812 return opts.func(opts)