blob: 6b884c18e929bbb221bc2dc686930c886de1380e [file] [log] [blame]
Kuang-che Wu88875db2017-07-20 10:47:53 +08001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Bisect command line interface."""
5
6from __future__ import print_function
7import argparse
8import datetime
9import logging
10import os
11import re
12import sys
13import textwrap
14import time
15
16from bisect_kit import common
17from bisect_kit import core
18from bisect_kit import strategy
19from bisect_kit import util
20
21logger = logging.getLogger(__name__)
22
23DEFAULT_SESSION_BASE = 'bisect.sessions'
24DEFAULT_SESSION_NAME = 'default'
25DEFAULT_CONFIDENCE = 0.999
26
27
28class ArgTypeError(argparse.ArgumentTypeError):
29 """An error for argument validation failure.
30
31 This not only tells users the argument is wrong but also gives correct
32 example. The main purpose of this error is for argtype_multiplexer, which
33 cascades examples from multiple ArgTypeError.
34 """
35
36 def __init__(self, msg, example):
37 self.msg = msg
38 if isinstance(example, list):
39 self.example = example
40 else:
41 self.example = [example]
42 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
43 super(ArgTypeError, self).__init__(full_msg)
44
45
46def argtype_notempty(s):
47 """Validates argument is not an empty string.
48
49 Args:
50 s: string to validate.
51
52 Raises:
53 ArgTypeError if argument is empty string.
54 """
55 if not s:
56 msg = 'should not be empty'
57 raise ArgTypeError(msg, 'foo')
58 return s
59
60
61def argtype_int(s):
62 """Validate argument is a number.
63
64 Args:
65 s: string to validate.
66
67 Raises:
68 ArgTypeError if argument is not a number.
69 """
70 try:
71 return str(int(s))
72 except ValueError:
73 raise ArgTypeError('should be a number', '123')
74
75
76def argtype_multiplexer(*args):
77 r"""argtype multiplexer
78
79 This function takes a list of argtypes or regex patterns and creates a new
80 function matching them. Moreover, it gives error message with examples.
81
82 Example:
83 >>> argtype = argtype_multiplexer(argtype_int, r'^r\d+$')
84 >>> argtype('123')
85 123
86 >>> argtype('r456')
87 r456
88 >>> argtype('hello')
89 ArgTypeError: Invalid argument (example value: 123, r\d+$)
90
91 Args:
92 *args: list of argtypes or regex pattern.
93
94 Returns:
95 A new argtype function which matches *args.
96 """
97
98 def validate(s):
99 examples = []
100 for t in args:
101 if isinstance(t, str):
102 if re.match(t, s):
103 return s
104 examples.append(t)
105 continue
106 try:
107 return t(s)
108 except ArgTypeError as e:
109 examples += e.example
110
111 msg = 'Invalid argument'
112 raise ArgTypeError(msg, examples)
113
114 return validate
115
116
117def argtype_multiplier(argtype):
118 """A new argtype that supports multiplier suffix of the given argtype.
119
120 Example:
121 Supports the given argtype accepting "foo" as argument, this function
122 generates a new argtype function which accepts argument like "foo*3".
123
124 Returns:
125 A new argtype function which returns (arg, times) where arg is accepted
126 by input `argtype` and times is repeating count. Note that if multiplier is
127 omitted, "times" is 1.
128 """
129
130 def helper(s):
131 m = re.match(r'^(.+)\*(\d+)$', s)
132 try:
133 if m:
134 return argtype(m.group(1)), int(m.group(2))
135 else:
136 return argtype(s), 1
137 except ArgTypeError as e:
138 # It should be okay to gives multiplier example only for the first one
139 # because it is just "example", no need to enumerate all possibilities.
140 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
141
142 return helper
143
144
145def argtype_dir_path(s):
146 """Validate argument is an existing directory.
147
148 Args:
149 s: string to validate.
150
151 Raises:
152 ArgTypeError if the path is not a directory.
153 """
154 if not os.path.exists(s):
155 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
156 if not os.path.isdir(s):
157 raise ArgTypeError('should be a directory', '/path/to/somewhere')
158
159 # Normalize, trim trailing path separators.
160 if len(s) > 1 and s[-1] == os.path.sep:
161 s = s[:-1]
162 return s
163
164
165def _collect_bisect_result_values(values, line):
166 """Collect bisect result values from output line.
167
168 Args:
169 values: Collected values are appending to this list.
170 line: One line of output string.
171 """
172 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
173 if m:
174 try:
175 values.extend(map(float, m.group(1).split()))
176 except ValueError:
177 raise core.ExecutionFatalError(
178 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
179
180
Kuang-che Wu88518882017-09-22 16:57:25 +0800181def check_executable(program):
182 """Checks whether a program is executable.
183
184 Args:
185 program: program path in question
186
187 Returns:
188 string as error message if `program` is not executable, or None otherwise.
189 It will return None if unable to determine as well.
190 """
191 returncode = util.call('which', program)
192 if returncode == 127: # No 'which' on this platform, skip the check.
193 return None
194 if returncode == 0: # is executable
195 return None
196
197 hint = ''
198 if not os.path.exists(program):
199 hint = 'Not in PATH?'
200 elif not os.path.isfile(program):
201 hint = 'Not a file'
202 elif not os.access(program, os.X_OK):
203 hint = 'Forgot to chmod +x?'
204 elif '/' not in program:
205 hint = 'Forgot to prepend "./" ?'
206 return '%r is not executable. %s' % (program, hint)
207
208
Kuang-che Wu88875db2017-07-20 10:47:53 +0800209def do_evaluate(evaluate_cmd, domain, rev):
210 """Invokes evaluator command.
211
212 The `evaluate_cmd` can get the target revision from the environment variable
213 named 'BISECT_REV'.
214
215 The result is determined according to the exit code of evaluator:
216 0: 'old'
217 1..124: 'new'
218 125: 'skip'
219 126, 127: fatal error
220 terminated by signal: fatal error
221
222 p.s. the definition of result is compatible with git-bisect(1).
223
224 It also extracts additional values from evaluate_cmd's stdout lines which
225 match the following format:
226 BISECT_RESULT_VALUES=<float>[, <float>]*
227
228 Args:
229 evaluate_cmd: evaluator command.
230 domain: a bisect_kit.core.Domain instance.
231 rev: version to evaluate.
232
233 Returns:
234 (result, values):
235 result is one of 'old', 'new', 'skip'.
236 values are additional collected values, like performance score.
237
238 Raises:
239 core.ExecutionFatalError if evaluator returned fatal error code.
240 """
241 env = os.environ.copy()
242 env['BISECT_REV'] = rev
243 domain.setenv(env, rev)
244
245 values = []
246 p = util.Popen(
247 evaluate_cmd,
248 env=env,
249 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
250 returncode = p.wait()
251 if returncode < 0 or returncode > 125:
252 raise core.ExecutionFatalError(str(returncode))
253
254 if returncode == 0:
255 return 'old', values
256 if returncode == 125:
257 return 'skip', values
258 return 'new', values
259
260
261def do_switch(switch_cmd, domain, rev):
262 """Invokes switcher command.
263
264 The `switch_cmd` can get the target revision from the environment variable
265 named 'BISECT_REV'.
266
267 The result is determined according to the exit code of switcher:
268 0: switch succeeded
269 1..125: 'skip'
270 126, 127: fatal error
271 terminated by signal: fatal error
272
273 In other words, any non-fatal errors are considered as 'skip'.
274
275 Args:
276 switch_cmd: switcher command.
277 domain: a bisect_kit.core.Domain instance.
278 rev: version to switch.
279
280 Returns:
281 None if switch successfully, 'skip' otherwise.
282
283 Raises:
284 core.ExecutionFatalError if switcher returned fatal error code.
285 """
286 env = os.environ.copy()
287 env['BISECT_REV'] = rev
288 domain.setenv(env, rev)
289
290 returncode = util.call(*switch_cmd, env=env)
291 if returncode < 0 or returncode > 125:
292 raise core.ExecutionFatalError(str(returncode))
293
294 if returncode != 0:
295 return 'skip'
296 return None
297
298
299# This is not an python "interface". pylint: disable=interface-not-implemented
300class BisectorCommandLineInterface(object):
301 """Bisector command line interface.
302
303 The typical usage pattern:
304
305 if __name__ == '__main__':
306 BisectorCommandLineInterface(CustomDomain).main()
307
308 where CustomDomain is a derived class of core.BisectDomain. See
309 bisect-list.py as example.
310
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)
345 logger.debug('revlist %r', revlist)
346 if 'new' not in config:
347 config['new'] = opts.new
348 if 'old' not in config:
349 config['old'] = opts.old
350 assert len(revlist) >= 2
351 assert config['new'] in revlist
352 assert config['old'] in revlist
353 old_idx = revlist.index(config['old'])
354 new_idx = revlist.index(config['new'])
355 assert old_idx < new_idx
356
357 config.update(confidence=opts.confidence, noisy=opts.noisy)
358
359 self.states.init(config, revlist)
360 self.states.save()
361
362 def _switch_and_eval(self, rev, prev_rev=None):
363 """Switches and evaluates given version.
364
365 If current version equals to target, switch step will be skip.
366
367 Args:
368 rev: Target version.
369 prev_rev: Previous version.
370
371 Returns:
372 (step, status, values):
373 step: Last step executed ('switch' or 'eval').
374 status: Execution result ('old', 'new', or 'skip').
375 values: Collected values from eval step. None if last step is 'switch'.
376 """
377 idx = self.states.rev2idx(rev)
378 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),
510 estimated_noise=','.join(estimated_noise),
511 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:
547 rev = self.domain_cls.revtype(opts.rev)
548 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:
580 defaults = util.DefaultConfig()
581 session_base = defaults.get('SESSION_BASE', DEFAULT_SESSION_BASE)
582
583 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
584
585 if self.states:
586 assert self.states.session_file == session_file
587 else:
588 self.states = core.States(session_file)
589
590 def cmd_config(self, opts):
591 """Configures additional setting.
592
593 See config command's help message for more detail.
594 """
595 self.states.load()
596 self.domain = self.domain_cls(self.states.config)
597 if not opts.value:
598 print(self.states.config[opts.key])
599 return
600
601 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800602 result = check_executable(opts.value[0])
603 if result:
604 raise core.ExecutionFatalError('%s config %s: %s' % (sys.argv[0],
605 opts.key, result))
606
Kuang-che Wu88875db2017-07-20 10:47:53 +0800607 self.states.config[opts.key] = opts.value
608
609 elif opts.key == 'confidence':
610 if len(opts.value) != 1:
611 raise core.ExecutionFatalError(
612 '%s config %s: expected 1 value, %d values given' %
613 (sys.argv[0], opts.key, len(opts.value)))
614 try:
615 self.states.config[opts.key] = float(opts.value[0])
616 except ValueError:
617 raise core.ExecutionFatalError('%s config %s: invalid float value: %r' %
618 (sys.argv[0], opts.key, opts.value[0]))
619
620 elif opts.key == 'noisy':
621 if len(opts.value) != 1:
622 raise core.ExecutionFatalError(
623 '%s config %s: expected 1 value, %d values given' %
624 (sys.argv[0], opts.key, len(opts.value)))
625 self.states.config[opts.key] = opts.value[0]
626
627 else:
628 raise core.ExecutionFatalError('%s config: unknown key: %r' %
629 (sys.argv[0], opts.key))
630
631 self.states.save()
632
633 def create_argument_parser(self, prog):
634 defaults = util.DefaultConfig()
635 parser = argparse.ArgumentParser(
636 prog=prog,
637 formatter_class=argparse.RawDescriptionHelpFormatter,
638 description=textwrap.dedent('''\
639 Bisector for %s.
640
641 When running switcher and evaluator, it will set BISECT_REV environment
642 variable, indicates current rev to switch/evaluate.
643 ''' % self.domain_cls.__name__) + textwrap.dedent(self.domain_cls.help))
644 common.add_logging_arguments(parser, defaults)
645 parser.add_argument(
646 '--session_base',
647 type=argtype_dir_path,
648 default=defaults.get('SESSION_BASE', DEFAULT_SESSION_BASE),
649 help='Directory to store sessions (default: %(default)r)')
650 parser.add_argument(
651 '--session',
652 default=DEFAULT_SESSION_NAME,
653 help='Session name (default: %(default)r)')
654 subparsers = parser.add_subparsers(
655 dest='command', title='commands', metavar='<command>')
656
657 parser_reset = subparsers.add_parser(
658 'reset', help='Reset bisect session and clean up saved result')
659 parser_reset.set_defaults(func=self.cmd_reset)
660
661 parser_init = subparsers.add_parser(
662 'init',
663 help='Initializes bisect session',
664 formatter_class=argparse.RawDescriptionHelpFormatter,
665 description=textwrap.dedent('''
666 Besides arguments for 'init' command, you also need to set 'switch'
667 and 'eval' command line via 'config' command.
668 $ bisector config switch <switch command and arguments>
669 $ bisector config eval <eval command and arguments>
670
671 The value of --noisy and --confidence could be changed by 'config'
672 command after 'init' as well.
673 '''))
674 parser_init.add_argument(
675 '--old',
676 required=True,
677 type=self.domain_cls.revtype,
678 help='Start of bisect range, which has old behavior')
679 parser_init.add_argument(
680 '--new',
681 required=True,
682 type=self.domain_cls.revtype,
683 help='End of bisect range, which has new behavior')
684 parser_init.add_argument(
685 '--noisy',
686 help='Enable noisy binary search and specify prior result. '
687 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
688 'and new fail rate increased to 2/3. '
689 'Skip if not flaky, say, "new=2/3" means old is always good.')
690 parser_init.add_argument(
691 '--confidence',
692 type=float,
693 default=DEFAULT_CONFIDENCE,
694 help='Confidence level (default: %(default)r)')
695 parser_init.set_defaults(func=self.cmd_init)
696 self.domain_cls.add_init_arguments(parser_init)
697
698 parser_config = subparsers.add_parser(
699 'config', help='Configures additional setting')
700 parser_config.add_argument(
701 'key',
702 choices=['switch', 'eval', 'confidence', 'noisy'],
703 metavar='key',
704 help='What config to change. choices=[%(choices)s]')
705 parser_config.add_argument(
706 'value', nargs=argparse.REMAINDER, help='New value')
707 parser_config.set_defaults(func=self.cmd_config)
708
709 parser_run = subparsers.add_parser(
710 'run',
711 help='Performs bisection',
712 formatter_class=argparse.RawDescriptionHelpFormatter,
713 description=textwrap.dedent('''
714 This command does switch and eval to determine candidates having old or
715 new behavior.
716
717 By default, it attempts to try versions in binary search manner until
718 found the first version having new behavior.
719
720 If version numbers are specified on command line, it just tries those
721 versions and record the result.
722
723 Example:
724 Bisect automatically.
725 $ %(prog)s
726
727 Switch and run version "2.13" and "2.14" and then stop.
728 $ %(prog)s 2.13 2.14
729 '''))
730 parser_run.add_argument(
731 '-1', '--once', action='store_true', help='Only run one step')
732 parser_run.add_argument(
733 'revs',
734 nargs='*',
735 type=self.domain_cls.revtype,
736 help='revs to switch+eval; '
737 'default is calculating automatically and run until done')
738 parser_run.set_defaults(func=self.cmd_run)
739
740 parser_switch = subparsers.add_parser(
741 'switch', help='Switch to given rev without eval')
742 parser_switch.add_argument(
743 'rev', type=argtype_multiplexer(self.domain_cls.revtype, 'next'))
744 parser_switch.set_defaults(func=self.cmd_switch)
745
746 parser_old = subparsers.add_parser(
747 'old', help='Tells bisect engine the said revs have "old" behavior')
748 parser_old.add_argument(
749 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
750 parser_old.set_defaults(func=self.cmd_old)
751
752 parser_new = subparsers.add_parser(
753 'new', help='Tells bisect engine the said revs have "new" behavior')
754 parser_new.add_argument(
755 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
756 parser_new.set_defaults(func=self.cmd_new)
757
758 parser_skip = subparsers.add_parser(
759 'skip', help='Tells bisect engine the said revs have "skip" behavior')
760 parser_skip.add_argument(
761 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
762 parser_skip.set_defaults(func=self.cmd_skip)
763
764 parser_view = subparsers.add_parser(
765 'view', help='Shows current progress and candidates')
766 parser_view.add_argument('--more', action='store_true')
767 parser_view.set_defaults(func=self.cmd_view)
768
769 parser_log = subparsers.add_parser(
770 'log', help='Prints what has been done so far')
771 parser_log.set_defaults(func=self.cmd_log)
772
773 parser_next = subparsers.add_parser(
774 'next', help='Prints next suggested rev to bisect')
775 parser_next.set_defaults(func=self.cmd_next)
776
777 return parser
778
779 def main(self, *args, **kwargs):
780 """Command line main function.
781
782 Args:
783 *args: Command line arguments.
784 **kwargs: additional non command line arguments passed by script code.
785 {
786 'prog': Program name; optional.
787 }
788 """
789 parser = self.create_argument_parser(kwargs.get('prog'))
790 opts = parser.parse_args(args or None)
791 common.config_logging(opts)
792
793 self._create_states(session=opts.session, session_base=opts.session_base)
794 if opts.command not in ('init', 'reset', 'config'):
795 self.states.load()
796 self.domain = self.domain_cls(self.states.config)
797 self.strategy = strategy.NoisyBinarySearch(
798 self.states.rev_info,
799 self.states.rev2idx(self.config['old']),
800 self.states.rev2idx(self.config['new']),
801 confidence=self.config['confidence'],
802 observation=self.config['noisy'])
803
804 return opts.func(opts)