blob: fa21ac3553302bca1fdb487fe11c676ed5ad9b25 [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)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800346 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800347 logger.debug('revlist %r', revlist)
348 if 'new' not in config:
349 config['new'] = opts.new
350 if 'old' not in config:
351 config['old'] = opts.old
352 assert len(revlist) >= 2
353 assert config['new'] in revlist
354 assert config['old'] in revlist
355 old_idx = revlist.index(config['old'])
356 new_idx = revlist.index(config['new'])
357 assert old_idx < new_idx
358
359 config.update(confidence=opts.confidence, noisy=opts.noisy)
360
361 self.states.init(config, revlist)
362 self.states.save()
363
364 def _switch_and_eval(self, rev, prev_rev=None):
365 """Switches and evaluates given version.
366
367 If current version equals to target, switch step will be skip.
368
369 Args:
370 rev: Target version.
371 prev_rev: Previous version.
372
373 Returns:
374 (step, status, values):
375 step: Last step executed ('switch' or 'eval').
376 status: Execution result ('old', 'new', or 'skip').
377 values: Collected values from eval step. None if last step is 'switch'.
378 """
379 idx = self.states.rev2idx(rev)
380 if prev_rev != rev:
381 logger.debug('switch to rev=%s', rev)
382 t0 = time.time()
383 status = do_switch(self.config['switch'], self.domain, rev)
384 t1 = time.time()
385 if status == 'skip':
386 logger.debug('switch failed => skip')
387 return 'switch', status, None
388 self.states.data['stats']['switch_count'] += 1
389 self.states.data['stats']['switch_time'] += t1 - t0
390
391 logger.debug('eval rev=%s', rev)
392 t0 = time.time()
393 status, values = do_evaluate(self.config['eval'], self.domain, rev)
394 t1 = time.time()
395 if status == 'skip':
396 return 'eval', status, values
397 self.states.data['stats']['eval_count'] += 1
398 self.states.data['stats']['eval_time'] += t1 - t0
399
400 return 'eval', status, values
401
402 def _next_idx_iter(self, opts):
403 if opts.revs:
404 for rev in opts.revs:
405 idx = self.states.rev2idx(rev)
406 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
407 yield idx, rev
408 if opts.once:
409 break
410 else:
411 while not self.strategy.is_done():
412 idx = self.strategy.next_idx()
413 rev = self.states.idx2rev(idx)
414 logger.info('try idx=%d rev=%s', idx, rev)
415 yield idx, rev
416 if opts.once:
417 break
418
419 def cmd_run(self, opts):
420 """Performs bisection.
421
422 See run command's help message for more detail.
423
424 Raises:
425 core.VerificationFailed: The bisection range cannot be verified. We
426 expect 'old' at the first rev and 'new' at last rev.
427 core.ExecutionFatalError: Fatal error, bisector stop.
428 strategy.WrongAssumption: Eval results contradicted.
429 """
430 assert self.config.get('switch')
431 assert self.config.get('eval')
432
433 self.strategy.rebuild()
434
435 prev_rev = None
436 for idx, rev in self._next_idx_iter(opts):
437 # Bail out if bisection range is unlikely true in order to prevent
438 # wasting time. This is necessary because some configurations (say,
439 # confidence) may be changed before cmd_run() and thus the bisection
440 # range becomes not acceptable.
441 self.strategy.check_verification_range()
442
443 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
444 logger.info('rev=%s => status %s', rev, status)
445
446 self._add_status(rev, status, values=values)
447 # Bail out if bisection range is unlikely true. Don't save.
448 # The last failing results are likely something wrong in bisection setup,
449 # bisector, and/or evaluator. The benefits of not saving the last failing
450 # results, are that users can just resume bisector directly without needs
451 # of erasing bad results after they fixed the problems.
452 self.strategy.check_verification_range()
453
454 if status == 'skip':
455 current_state = self.states.get(idx)
456 # Bail out if 'skip' too much times. Don't save.
457 if current_state['skip'] > (
458 current_state['old'] + current_state['new'] + 1) * 5:
459 raise core.ExecutionFatalError('too much "skip" for rev=%r' % rev)
460
461 self.states.save()
462
463 self.strategy.show_summary()
464
465 if step == 'switch' and status == 'skip':
466 # Previous switch failed and thus the current version is unknown. Set
467 # it None, so next switch operation won't be bypassed (due to
468 # optimization).
469 prev_rev = None
470 else:
471 prev_rev = rev
472
473 logger.info('done')
474
475 def cmd_view(self, opts):
476 """Shows current progress and candidates."""
477 self.strategy.rebuild()
478 # Rebuild twice in order to re-estimate noise.
479 self.strategy.rebuild()
480 self.strategy.show_summary(more=opts.more)
481 left, right = self.strategy.get_range()
482 self.domain.view(self.states.idx2rev(left), self.states.idx2rev(right))
483
484 def current_status(self, session=None, session_base=None):
485 """Gets current bisect status.
486
487 Returns:
488 A dict describing current status. It contains following items:
489 inited: True iff the session file is initialized (init command has been
490 invoked). If not, below items are omitted.
491 old: Start of current estimated range.
492 new: End of current estimated range.
493 estimated_noise: New estimated noise.
494 done: True if bisection is done, otherwise False.
495 """
496 self._create_states(session=session, session_base=session_base)
497 if self.states.load():
498 self.strategy = strategy.NoisyBinarySearch(
499 self.states.rev_info,
500 self.states.rev2idx(self.config['old']),
501 self.states.rev2idx(self.config['new']),
502 confidence=self.config['confidence'],
503 observation=self.config['noisy'])
504 self.strategy.rebuild()
505 left, right = self.strategy.get_range()
506 estimated_noise = self.strategy.get_noise_observation()
507
508 result = dict(
509 inited=True,
510 old=self.states.idx2rev(left),
511 new=self.states.idx2rev(right),
512 estimated_noise=','.join(estimated_noise),
513 done=self.strategy.is_done())
514 else:
515 result = dict(inited=False)
516 return result
517
518 def cmd_log(self, _opts):
519 """Prints what has been done so far."""
520 for entry in self.states.data['history']:
521 print('{datetime}, {rev} {status_time} {values} {comment_display}'.format(
522 datetime=datetime.datetime.fromtimestamp(entry['time']),
523 status_time=entry['status'] + ('*%d' % entry['times']
524 if entry['times'] > 1 else ''),
525 comment_display=entry['comment'] or '',
526 **entry))
527
528 def cmd_next(self, _opts):
529 """Prints next suggested rev to bisect."""
530 self.strategy.rebuild()
531 if self.strategy.is_done():
532 print('done')
533 return
534
535 idx = self.strategy.next_idx()
536 rev = self.states.idx2rev(idx)
537 print(rev)
538
539 def cmd_switch(self, opts):
540 """Switches to given rev without eval."""
541 assert self.config.get('switch')
542
543 self.strategy.rebuild()
544
545 if opts.rev == 'next':
546 idx = self.strategy.next_idx()
547 rev = self.states.idx2rev(idx)
548 else:
549 rev = self.domain_cls.revtype(opts.rev)
550 assert rev
551
552 logger.info('switch to %s', rev)
553 status = do_switch(self.config['switch'], self.domain, rev)
554 if status:
555 print('switch failed')
556
557 def _add_revs_status_helper(self, revs, status):
558 self.strategy.rebuild()
559 for rev, times in revs:
560 self._add_status(rev, status, times=times, comment='manual')
561 self.states.save()
562
563 def cmd_new(self, opts):
564 """Tells bisect engine the said revs have "new" behavior."""
565 logger.info('set [%s] as new', opts.revs)
566 self._add_revs_status_helper(opts.revs, 'new')
567
568 def cmd_old(self, opts):
569 """Tells bisect engine the said revs have "old" behavior."""
570 logger.info('set [%s] as old', opts.revs)
571 self._add_revs_status_helper(opts.revs, 'old')
572
573 def cmd_skip(self, opts):
574 """Tells bisect engine the said revs have "skip" behavior."""
575 logger.info('set [%s] as skip', opts.revs)
576 self._add_revs_status_helper(opts.revs, 'skip')
577
578 def _create_states(self, session=None, session_base=None):
579 if not session:
580 session = DEFAULT_SESSION_NAME
581 if not session_base:
Kuang-che Wu385279d2017-09-27 14:48:28 +0800582 session_base = configure.get('SESSION_BASE', DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800583
584 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
585
586 if self.states:
587 assert self.states.session_file == session_file
588 else:
589 self.states = core.States(session_file)
590
591 def cmd_config(self, opts):
592 """Configures additional setting.
593
594 See config command's help message for more detail.
595 """
596 self.states.load()
597 self.domain = self.domain_cls(self.states.config)
598 if not opts.value:
599 print(self.states.config[opts.key])
600 return
601
602 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800603 result = check_executable(opts.value[0])
604 if result:
605 raise core.ExecutionFatalError('%s config %s: %s' % (sys.argv[0],
606 opts.key, result))
607
Kuang-che Wu88875db2017-07-20 10:47:53 +0800608 self.states.config[opts.key] = opts.value
609
610 elif opts.key == 'confidence':
611 if len(opts.value) != 1:
612 raise core.ExecutionFatalError(
613 '%s config %s: expected 1 value, %d values given' %
614 (sys.argv[0], opts.key, len(opts.value)))
615 try:
616 self.states.config[opts.key] = float(opts.value[0])
617 except ValueError:
618 raise core.ExecutionFatalError('%s config %s: invalid float value: %r' %
619 (sys.argv[0], opts.key, opts.value[0]))
620
621 elif opts.key == 'noisy':
622 if len(opts.value) != 1:
623 raise core.ExecutionFatalError(
624 '%s config %s: expected 1 value, %d values given' %
625 (sys.argv[0], opts.key, len(opts.value)))
626 self.states.config[opts.key] = opts.value[0]
627
628 else:
629 raise core.ExecutionFatalError('%s config: unknown key: %r' %
630 (sys.argv[0], opts.key))
631
632 self.states.save()
633
634 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800635 if self.domain_cls.help:
636 description = self.domain_cls.help
637 else:
638 description = 'Bisector for %s' % self.domain_cls.__name__
639 description += textwrap.dedent('''
640 When running switcher and evaluator, it will set BISECT_REV environment
641 variable, indicates current rev to switch/evaluate.
642 ''')
643
Kuang-che Wu88875db2017-07-20 10:47:53 +0800644 parser = argparse.ArgumentParser(
645 prog=prog,
646 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800647 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800648 common.add_common_arguments(parser)
649 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800650 '--session_base',
651 type=argtype_dir_path,
Kuang-che Wu385279d2017-09-27 14:48:28 +0800652 default=configure.get('SESSION_BASE', DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800653 help='Directory to store sessions (default: %(default)r)')
654 parser.add_argument(
655 '--session',
656 default=DEFAULT_SESSION_NAME,
657 help='Session name (default: %(default)r)')
658 subparsers = parser.add_subparsers(
659 dest='command', title='commands', metavar='<command>')
660
661 parser_reset = subparsers.add_parser(
662 'reset', help='Reset bisect session and clean up saved result')
663 parser_reset.set_defaults(func=self.cmd_reset)
664
665 parser_init = subparsers.add_parser(
666 'init',
667 help='Initializes bisect session',
668 formatter_class=argparse.RawDescriptionHelpFormatter,
669 description=textwrap.dedent('''
670 Besides arguments for 'init' command, you also need to set 'switch'
671 and 'eval' command line via 'config' command.
672 $ bisector config switch <switch command and arguments>
673 $ bisector config eval <eval command and arguments>
674
675 The value of --noisy and --confidence could be changed by 'config'
676 command after 'init' as well.
677 '''))
678 parser_init.add_argument(
679 '--old',
680 required=True,
681 type=self.domain_cls.revtype,
682 help='Start of bisect range, which has old behavior')
683 parser_init.add_argument(
684 '--new',
685 required=True,
686 type=self.domain_cls.revtype,
687 help='End of bisect range, which has new behavior')
688 parser_init.add_argument(
689 '--noisy',
690 help='Enable noisy binary search and specify prior result. '
691 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
692 'and new fail rate increased to 2/3. '
693 'Skip if not flaky, say, "new=2/3" means old is always good.')
694 parser_init.add_argument(
695 '--confidence',
696 type=float,
697 default=DEFAULT_CONFIDENCE,
698 help='Confidence level (default: %(default)r)')
699 parser_init.set_defaults(func=self.cmd_init)
700 self.domain_cls.add_init_arguments(parser_init)
701
702 parser_config = subparsers.add_parser(
703 'config', help='Configures additional setting')
704 parser_config.add_argument(
705 'key',
706 choices=['switch', 'eval', 'confidence', 'noisy'],
707 metavar='key',
708 help='What config to change. choices=[%(choices)s]')
709 parser_config.add_argument(
710 'value', nargs=argparse.REMAINDER, help='New value')
711 parser_config.set_defaults(func=self.cmd_config)
712
713 parser_run = subparsers.add_parser(
714 'run',
715 help='Performs bisection',
716 formatter_class=argparse.RawDescriptionHelpFormatter,
717 description=textwrap.dedent('''
718 This command does switch and eval to determine candidates having old or
719 new behavior.
720
721 By default, it attempts to try versions in binary search manner until
722 found the first version having new behavior.
723
724 If version numbers are specified on command line, it just tries those
725 versions and record the result.
726
727 Example:
728 Bisect automatically.
729 $ %(prog)s
730
731 Switch and run version "2.13" and "2.14" and then stop.
732 $ %(prog)s 2.13 2.14
733 '''))
734 parser_run.add_argument(
735 '-1', '--once', action='store_true', help='Only run one step')
736 parser_run.add_argument(
737 'revs',
738 nargs='*',
739 type=self.domain_cls.revtype,
740 help='revs to switch+eval; '
741 'default is calculating automatically and run until done')
742 parser_run.set_defaults(func=self.cmd_run)
743
744 parser_switch = subparsers.add_parser(
745 'switch', help='Switch to given rev without eval')
746 parser_switch.add_argument(
747 'rev', type=argtype_multiplexer(self.domain_cls.revtype, 'next'))
748 parser_switch.set_defaults(func=self.cmd_switch)
749
750 parser_old = subparsers.add_parser(
751 'old', help='Tells bisect engine the said revs have "old" behavior')
752 parser_old.add_argument(
753 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
754 parser_old.set_defaults(func=self.cmd_old)
755
756 parser_new = subparsers.add_parser(
757 'new', help='Tells bisect engine the said revs have "new" behavior')
758 parser_new.add_argument(
759 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
760 parser_new.set_defaults(func=self.cmd_new)
761
762 parser_skip = subparsers.add_parser(
763 'skip', help='Tells bisect engine the said revs have "skip" behavior')
764 parser_skip.add_argument(
765 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
766 parser_skip.set_defaults(func=self.cmd_skip)
767
768 parser_view = subparsers.add_parser(
769 'view', help='Shows current progress and candidates')
770 parser_view.add_argument('--more', action='store_true')
771 parser_view.set_defaults(func=self.cmd_view)
772
773 parser_log = subparsers.add_parser(
774 'log', help='Prints what has been done so far')
775 parser_log.set_defaults(func=self.cmd_log)
776
777 parser_next = subparsers.add_parser(
778 'next', help='Prints next suggested rev to bisect')
779 parser_next.set_defaults(func=self.cmd_next)
780
781 return parser
782
783 def main(self, *args, **kwargs):
784 """Command line main function.
785
786 Args:
787 *args: Command line arguments.
788 **kwargs: additional non command line arguments passed by script code.
789 {
790 'prog': Program name; optional.
791 }
792 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800793 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800794 parser = self.create_argument_parser(kwargs.get('prog'))
795 opts = parser.parse_args(args or None)
796 common.config_logging(opts)
797
798 self._create_states(session=opts.session, session_base=opts.session_base)
799 if opts.command not in ('init', 'reset', 'config'):
800 self.states.load()
801 self.domain = self.domain_cls(self.states.config)
802 self.strategy = strategy.NoisyBinarySearch(
803 self.states.rev_info,
804 self.states.rev2idx(self.config['old']),
805 self.states.rev2idx(self.config['new']),
806 confidence=self.config['confidence'],
807 observation=self.config['noisy'])
808
809 return opts.func(opts)