blob: 4a87c32416407fd4dbf06e9d96d734c833dd08a0 [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
Kuang-che Wu385279d2017-09-27 14:48:28 +080017from bisect_kit import configure
Kuang-che Wu88875db2017-07-20 10:47:53 +080018from bisect_kit import core
19from bisect_kit import strategy
20from bisect_kit import util
21
22logger = logging.getLogger(__name__)
23
24DEFAULT_SESSION_BASE = 'bisect.sessions'
25DEFAULT_SESSION_NAME = 'default'
26DEFAULT_CONFIDENCE = 0.999
27
28
29class ArgTypeError(argparse.ArgumentTypeError):
30 """An error for argument validation failure.
31
32 This not only tells users the argument is wrong but also gives correct
33 example. The main purpose of this error is for argtype_multiplexer, which
34 cascades examples from multiple ArgTypeError.
35 """
36
37 def __init__(self, msg, example):
38 self.msg = msg
39 if isinstance(example, list):
40 self.example = example
41 else:
42 self.example = [example]
43 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
44 super(ArgTypeError, self).__init__(full_msg)
45
46
47def argtype_notempty(s):
48 """Validates argument is not an empty string.
49
50 Args:
51 s: string to validate.
52
53 Raises:
54 ArgTypeError if argument is empty string.
55 """
56 if not s:
57 msg = 'should not be empty'
58 raise ArgTypeError(msg, 'foo')
59 return s
60
61
62def argtype_int(s):
63 """Validate argument is a number.
64
65 Args:
66 s: string to validate.
67
68 Raises:
69 ArgTypeError if argument is not a number.
70 """
71 try:
72 return str(int(s))
73 except ValueError:
74 raise ArgTypeError('should be a number', '123')
75
76
77def argtype_multiplexer(*args):
78 r"""argtype multiplexer
79
80 This function takes a list of argtypes or regex patterns and creates a new
81 function matching them. Moreover, it gives error message with examples.
82
83 Example:
84 >>> argtype = argtype_multiplexer(argtype_int, r'^r\d+$')
85 >>> argtype('123')
86 123
87 >>> argtype('r456')
88 r456
89 >>> argtype('hello')
90 ArgTypeError: Invalid argument (example value: 123, r\d+$)
91
92 Args:
93 *args: list of argtypes or regex pattern.
94
95 Returns:
96 A new argtype function which matches *args.
97 """
98
99 def validate(s):
100 examples = []
101 for t in args:
102 if isinstance(t, str):
103 if re.match(t, s):
104 return s
105 examples.append(t)
106 continue
107 try:
108 return t(s)
109 except ArgTypeError as e:
110 examples += e.example
111
112 msg = 'Invalid argument'
113 raise ArgTypeError(msg, examples)
114
115 return validate
116
117
118def argtype_multiplier(argtype):
119 """A new argtype that supports multiplier suffix of the given argtype.
120
121 Example:
122 Supports the given argtype accepting "foo" as argument, this function
123 generates a new argtype function which accepts argument like "foo*3".
124
125 Returns:
126 A new argtype function which returns (arg, times) where arg is accepted
127 by input `argtype` and times is repeating count. Note that if multiplier is
128 omitted, "times" is 1.
129 """
130
131 def helper(s):
132 m = re.match(r'^(.+)\*(\d+)$', s)
133 try:
134 if m:
135 return argtype(m.group(1)), int(m.group(2))
136 else:
137 return argtype(s), 1
138 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
300# This is not an python "interface". pylint: disable=interface-not-implemented
301class BisectorCommandLineInterface(object):
302 """Bisector command line interface.
303
304 The typical usage pattern:
305
306 if __name__ == '__main__':
307 BisectorCommandLineInterface(CustomDomain).main()
308
309 where CustomDomain is a derived class of core.BisectDomain. See
310 bisect-list.py as example.
311
312 If you need to control the bisector using python code, the easier way is
313 passing command line arguments to main() function. For example,
314 bisector = Bisector(CustomDomain)
315 bisector.main('init', '--old', '123', '--new', '456')
316 bisector.main('config', 'switch', 'true')
317 bisector.main('config', 'eval', 'true')
318 bisector.main('run')
319 """
320
321 def __init__(self, domain_cls):
322 self.domain_cls = domain_cls
323 self.domain = None
324 self.states = None
325 self.strategy = None
326
327 @property
328 def config(self):
329 return self.states.config
330
331 def _add_status(self, rev, status, **kwargs):
332 idx = self.states.rev2idx(rev)
333 self.states.add_status(idx, status, **kwargs)
334 self.strategy.update(idx, status)
335
336 def cmd_reset(self, _opts):
337 """Resets bisect session and clean up saved result."""
338 self.states.reset()
339
340 def cmd_init(self, opts):
341 """Initializes bisect session.
342
343 See init command's help message for more detail.
344 """
345 config, revlist = self.domain_cls.init(opts)
346 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 """
378 idx = self.states.rev2idx(rev)
379 if prev_rev != rev:
380 logger.debug('switch to rev=%s', rev)
381 t0 = time.time()
382 status = do_switch(self.config['switch'], self.domain, rev)
383 t1 = time.time()
384 if status == 'skip':
385 logger.debug('switch failed => skip')
386 return 'switch', status, None
387 self.states.data['stats']['switch_count'] += 1
388 self.states.data['stats']['switch_time'] += t1 - t0
389
390 logger.debug('eval rev=%s', rev)
391 t0 = time.time()
392 status, values = do_evaluate(self.config['eval'], self.domain, rev)
393 t1 = time.time()
394 if status == 'skip':
395 return 'eval', status, values
396 self.states.data['stats']['eval_count'] += 1
397 self.states.data['stats']['eval_time'] += t1 - t0
398
399 return 'eval', status, values
400
401 def _next_idx_iter(self, opts):
402 if opts.revs:
403 for rev in opts.revs:
404 idx = self.states.rev2idx(rev)
405 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
406 yield idx, rev
407 if opts.once:
408 break
409 else:
410 while not self.strategy.is_done():
411 idx = self.strategy.next_idx()
412 rev = self.states.idx2rev(idx)
413 logger.info('try idx=%d rev=%s', idx, rev)
414 yield idx, rev
415 if opts.once:
416 break
417
418 def cmd_run(self, opts):
419 """Performs bisection.
420
421 See run command's help message for more detail.
422
423 Raises:
424 core.VerificationFailed: The bisection range cannot be verified. We
425 expect 'old' at the first rev and 'new' at last rev.
426 core.ExecutionFatalError: Fatal error, bisector stop.
427 strategy.WrongAssumption: Eval results contradicted.
428 """
429 assert self.config.get('switch')
430 assert self.config.get('eval')
431
432 self.strategy.rebuild()
433
434 prev_rev = None
435 for idx, rev in self._next_idx_iter(opts):
436 # Bail out if bisection range is unlikely true in order to prevent
437 # wasting time. This is necessary because some configurations (say,
438 # confidence) may be changed before cmd_run() and thus the bisection
439 # range becomes not acceptable.
440 self.strategy.check_verification_range()
441
442 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
443 logger.info('rev=%s => status %s', rev, status)
444
445 self._add_status(rev, status, values=values)
446 # Bail out if bisection range is unlikely true. Don't save.
447 # The last failing results are likely something wrong in bisection setup,
448 # bisector, and/or evaluator. The benefits of not saving the last failing
449 # results, are that users can just resume bisector directly without needs
450 # of erasing bad results after they fixed the problems.
451 self.strategy.check_verification_range()
452
453 if status == 'skip':
454 current_state = self.states.get(idx)
455 # Bail out if 'skip' too much times. Don't save.
456 if current_state['skip'] > (
457 current_state['old'] + current_state['new'] + 1) * 5:
458 raise core.ExecutionFatalError('too much "skip" for rev=%r' % rev)
459
460 self.states.save()
461
462 self.strategy.show_summary()
463
464 if step == 'switch' and status == 'skip':
465 # Previous switch failed and thus the current version is unknown. Set
466 # it None, so next switch operation won't be bypassed (due to
467 # optimization).
468 prev_rev = None
469 else:
470 prev_rev = rev
471
472 logger.info('done')
473
474 def cmd_view(self, opts):
475 """Shows current progress and candidates."""
476 self.strategy.rebuild()
477 # Rebuild twice in order to re-estimate noise.
478 self.strategy.rebuild()
479 self.strategy.show_summary(more=opts.more)
480 left, right = self.strategy.get_range()
481 self.domain.view(self.states.idx2rev(left), self.states.idx2rev(right))
482
483 def current_status(self, session=None, session_base=None):
484 """Gets current bisect status.
485
486 Returns:
487 A dict describing current status. It contains following items:
488 inited: True iff the session file is initialized (init command has been
489 invoked). If not, below items are omitted.
490 old: Start of current estimated range.
491 new: End of current estimated range.
492 estimated_noise: New estimated noise.
493 done: True if bisection is done, otherwise False.
494 """
495 self._create_states(session=session, session_base=session_base)
496 if self.states.load():
497 self.strategy = strategy.NoisyBinarySearch(
498 self.states.rev_info,
499 self.states.rev2idx(self.config['old']),
500 self.states.rev2idx(self.config['new']),
501 confidence=self.config['confidence'],
502 observation=self.config['noisy'])
503 self.strategy.rebuild()
504 left, right = self.strategy.get_range()
505 estimated_noise = self.strategy.get_noise_observation()
506
507 result = dict(
508 inited=True,
509 old=self.states.idx2rev(left),
510 new=self.states.idx2rev(right),
511 estimated_noise=','.join(estimated_noise),
512 done=self.strategy.is_done())
513 else:
514 result = dict(inited=False)
515 return result
516
517 def cmd_log(self, _opts):
518 """Prints what has been done so far."""
519 for entry in self.states.data['history']:
520 print('{datetime}, {rev} {status_time} {values} {comment_display}'.format(
521 datetime=datetime.datetime.fromtimestamp(entry['time']),
522 status_time=entry['status'] + ('*%d' % entry['times']
523 if entry['times'] > 1 else ''),
524 comment_display=entry['comment'] or '',
525 **entry))
526
527 def cmd_next(self, _opts):
528 """Prints next suggested rev to bisect."""
529 self.strategy.rebuild()
530 if self.strategy.is_done():
531 print('done')
532 return
533
534 idx = self.strategy.next_idx()
535 rev = self.states.idx2rev(idx)
536 print(rev)
537
538 def cmd_switch(self, opts):
539 """Switches to given rev without eval."""
540 assert self.config.get('switch')
541
542 self.strategy.rebuild()
543
544 if opts.rev == 'next':
545 idx = self.strategy.next_idx()
546 rev = self.states.idx2rev(idx)
547 else:
548 rev = self.domain_cls.revtype(opts.rev)
549 assert rev
550
551 logger.info('switch to %s', rev)
552 status = do_switch(self.config['switch'], self.domain, rev)
553 if status:
554 print('switch failed')
555
556 def _add_revs_status_helper(self, revs, status):
557 self.strategy.rebuild()
558 for rev, times in revs:
559 self._add_status(rev, status, times=times, comment='manual')
560 self.states.save()
561
562 def cmd_new(self, opts):
563 """Tells bisect engine the said revs have "new" behavior."""
564 logger.info('set [%s] as new', opts.revs)
565 self._add_revs_status_helper(opts.revs, 'new')
566
567 def cmd_old(self, opts):
568 """Tells bisect engine the said revs have "old" behavior."""
569 logger.info('set [%s] as old', opts.revs)
570 self._add_revs_status_helper(opts.revs, 'old')
571
572 def cmd_skip(self, opts):
573 """Tells bisect engine the said revs have "skip" behavior."""
574 logger.info('set [%s] as skip', opts.revs)
575 self._add_revs_status_helper(opts.revs, 'skip')
576
577 def _create_states(self, session=None, session_base=None):
578 if not session:
579 session = DEFAULT_SESSION_NAME
580 if not session_base:
Kuang-che Wu385279d2017-09-27 14:48:28 +0800581 session_base = configure.get('SESSION_BASE', DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800582
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):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800634 parser = argparse.ArgumentParser(
635 prog=prog,
636 formatter_class=argparse.RawDescriptionHelpFormatter,
637 description=textwrap.dedent('''\
638 Bisector for %s.
639
640 When running switcher and evaluator, it will set BISECT_REV environment
641 variable, indicates current rev to switch/evaluate.
642 ''' % self.domain_cls.__name__) + textwrap.dedent(self.domain_cls.help))
Kuang-che Wu385279d2017-09-27 14:48:28 +0800643 common.add_common_arguments(parser)
644 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800645 '--session_base',
646 type=argtype_dir_path,
Kuang-che Wu385279d2017-09-27 14:48:28 +0800647 default=configure.get('SESSION_BASE', DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800648 help='Directory to store sessions (default: %(default)r)')
649 parser.add_argument(
650 '--session',
651 default=DEFAULT_SESSION_NAME,
652 help='Session name (default: %(default)r)')
653 subparsers = parser.add_subparsers(
654 dest='command', title='commands', metavar='<command>')
655
656 parser_reset = subparsers.add_parser(
657 'reset', help='Reset bisect session and clean up saved result')
658 parser_reset.set_defaults(func=self.cmd_reset)
659
660 parser_init = subparsers.add_parser(
661 'init',
662 help='Initializes bisect session',
663 formatter_class=argparse.RawDescriptionHelpFormatter,
664 description=textwrap.dedent('''
665 Besides arguments for 'init' command, you also need to set 'switch'
666 and 'eval' command line via 'config' command.
667 $ bisector config switch <switch command and arguments>
668 $ bisector config eval <eval command and arguments>
669
670 The value of --noisy and --confidence could be changed by 'config'
671 command after 'init' as well.
672 '''))
673 parser_init.add_argument(
674 '--old',
675 required=True,
676 type=self.domain_cls.revtype,
677 help='Start of bisect range, which has old behavior')
678 parser_init.add_argument(
679 '--new',
680 required=True,
681 type=self.domain_cls.revtype,
682 help='End of bisect range, which has new behavior')
683 parser_init.add_argument(
684 '--noisy',
685 help='Enable noisy binary search and specify prior result. '
686 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
687 'and new fail rate increased to 2/3. '
688 'Skip if not flaky, say, "new=2/3" means old is always good.')
689 parser_init.add_argument(
690 '--confidence',
691 type=float,
692 default=DEFAULT_CONFIDENCE,
693 help='Confidence level (default: %(default)r)')
694 parser_init.set_defaults(func=self.cmd_init)
695 self.domain_cls.add_init_arguments(parser_init)
696
697 parser_config = subparsers.add_parser(
698 'config', help='Configures additional setting')
699 parser_config.add_argument(
700 'key',
701 choices=['switch', 'eval', 'confidence', 'noisy'],
702 metavar='key',
703 help='What config to change. choices=[%(choices)s]')
704 parser_config.add_argument(
705 'value', nargs=argparse.REMAINDER, help='New value')
706 parser_config.set_defaults(func=self.cmd_config)
707
708 parser_run = subparsers.add_parser(
709 'run',
710 help='Performs bisection',
711 formatter_class=argparse.RawDescriptionHelpFormatter,
712 description=textwrap.dedent('''
713 This command does switch and eval to determine candidates having old or
714 new behavior.
715
716 By default, it attempts to try versions in binary search manner until
717 found the first version having new behavior.
718
719 If version numbers are specified on command line, it just tries those
720 versions and record the result.
721
722 Example:
723 Bisect automatically.
724 $ %(prog)s
725
726 Switch and run version "2.13" and "2.14" and then stop.
727 $ %(prog)s 2.13 2.14
728 '''))
729 parser_run.add_argument(
730 '-1', '--once', action='store_true', help='Only run one step')
731 parser_run.add_argument(
732 'revs',
733 nargs='*',
734 type=self.domain_cls.revtype,
735 help='revs to switch+eval; '
736 'default is calculating automatically and run until done')
737 parser_run.set_defaults(func=self.cmd_run)
738
739 parser_switch = subparsers.add_parser(
740 'switch', help='Switch to given rev without eval')
741 parser_switch.add_argument(
742 'rev', type=argtype_multiplexer(self.domain_cls.revtype, 'next'))
743 parser_switch.set_defaults(func=self.cmd_switch)
744
745 parser_old = subparsers.add_parser(
746 'old', help='Tells bisect engine the said revs have "old" behavior')
747 parser_old.add_argument(
748 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
749 parser_old.set_defaults(func=self.cmd_old)
750
751 parser_new = subparsers.add_parser(
752 'new', help='Tells bisect engine the said revs have "new" behavior')
753 parser_new.add_argument(
754 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
755 parser_new.set_defaults(func=self.cmd_new)
756
757 parser_skip = subparsers.add_parser(
758 'skip', help='Tells bisect engine the said revs have "skip" behavior')
759 parser_skip.add_argument(
760 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
761 parser_skip.set_defaults(func=self.cmd_skip)
762
763 parser_view = subparsers.add_parser(
764 'view', help='Shows current progress and candidates')
765 parser_view.add_argument('--more', action='store_true')
766 parser_view.set_defaults(func=self.cmd_view)
767
768 parser_log = subparsers.add_parser(
769 'log', help='Prints what has been done so far')
770 parser_log.set_defaults(func=self.cmd_log)
771
772 parser_next = subparsers.add_parser(
773 'next', help='Prints next suggested rev to bisect')
774 parser_next.set_defaults(func=self.cmd_next)
775
776 return parser
777
778 def main(self, *args, **kwargs):
779 """Command line main function.
780
781 Args:
782 *args: Command line arguments.
783 **kwargs: additional non command line arguments passed by script code.
784 {
785 'prog': Program name; optional.
786 }
787 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800788 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800789 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)