blob: ef54dbdbf28692bef880e79ddf91361a5187dc0a [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
Kuang-che Wu88875db2017-07-20 10:47:53 +080025DEFAULT_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
Kuang-che Wubaaa4532018-08-15 17:08:10 +080083 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +080084 >>> 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
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800121 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800122 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))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800136 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800137 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
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800299class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800300 """Bisector command line interface.
301
302 The typical usage pattern:
303
304 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800305 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800306
307 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800308 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800309
310 If you need to control the bisector using python code, the easier way is
311 passing command line arguments to main() function. For example,
312 bisector = Bisector(CustomDomain)
313 bisector.main('init', '--old', '123', '--new', '456')
314 bisector.main('config', 'switch', 'true')
315 bisector.main('config', 'eval', 'true')
316 bisector.main('run')
317 """
318
319 def __init__(self, domain_cls):
320 self.domain_cls = domain_cls
321 self.domain = None
322 self.states = None
323 self.strategy = None
324
325 @property
326 def config(self):
327 return self.states.config
328
329 def _add_status(self, rev, status, **kwargs):
330 idx = self.states.rev2idx(rev)
331 self.states.add_status(idx, status, **kwargs)
332 self.strategy.update(idx, status)
333
334 def cmd_reset(self, _opts):
335 """Resets bisect session and clean up saved result."""
336 self.states.reset()
337
338 def cmd_init(self, opts):
339 """Initializes bisect session.
340
341 See init command's help message for more detail.
342 """
343 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800344 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800345 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 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800377 if prev_rev != rev:
378 logger.debug('switch to rev=%s', rev)
379 t0 = time.time()
380 status = do_switch(self.config['switch'], self.domain, rev)
381 t1 = time.time()
382 if status == 'skip':
383 logger.debug('switch failed => skip')
384 return 'switch', status, None
385 self.states.data['stats']['switch_count'] += 1
386 self.states.data['stats']['switch_time'] += t1 - t0
387
388 logger.debug('eval rev=%s', rev)
389 t0 = time.time()
390 status, values = do_evaluate(self.config['eval'], self.domain, rev)
391 t1 = time.time()
392 if status == 'skip':
393 return 'eval', status, values
394 self.states.data['stats']['eval_count'] += 1
395 self.states.data['stats']['eval_time'] += t1 - t0
396
397 return 'eval', status, values
398
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800399 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800400 if opts.revs:
401 for rev in opts.revs:
402 idx = self.states.rev2idx(rev)
403 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
404 yield idx, rev
405 if opts.once:
406 break
407 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800408 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800409 idx = self.strategy.next_idx()
410 rev = self.states.idx2rev(idx)
411 logger.info('try idx=%d rev=%s', idx, rev)
412 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800413 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800414 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
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800434 force = opts.force
435 for idx, rev in self._next_idx_iter(opts, force):
436 if not force:
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()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800442
443 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
444 logger.info('rev=%s => status %s', rev, status)
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800445 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800446
447 self._add_status(rev, status, values=values)
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800448 self.states.save()
449
450 # Bail out if bisection range is unlikely true.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800451 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
Kuang-che Wu88875db2017-07-20 10:47:53 +0800460 self.strategy.show_summary()
461
462 if step == 'switch' and status == 'skip':
463 # Previous switch failed and thus the current version is unknown. Set
464 # it None, so next switch operation won't be bypassed (due to
465 # optimization).
466 prev_rev = None
467 else:
468 prev_rev = rev
469
470 logger.info('done')
471
472 def cmd_view(self, opts):
473 """Shows current progress and candidates."""
474 self.strategy.rebuild()
475 # Rebuild twice in order to re-estimate noise.
476 self.strategy.rebuild()
477 self.strategy.show_summary(more=opts.more)
478 left, right = self.strategy.get_range()
Kuang-che Wu81aecc02018-10-31 19:37:32 +0800479 self.domain.view(self.states.data['revlist'], self.states.idx2rev(left),
480 self.states.idx2rev(right))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800481
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']:
Kuang-che Wu849da582018-09-06 18:11:52 +0800519 print('{datetime}, {rev} {status} {values} {comment}'.format(
520 datetime=datetime.datetime.fromtimestamp(entry['timestamp']),
521 rev=entry['rev'],
522 status=entry['status'] + ('*%d' % entry['times']
523 if entry.get('times', 1) > 1 else ''),
524 values=entry.get('values', ''),
525 comment=entry.get('comment', '')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800526
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:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800548 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800549 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 Wu41e8b592018-09-25 17:01:30 +0800581 session_base = configure.get('SESSION_BASE', common.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:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800588 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800589
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:
Kuang-che Wu74768d32018-09-07 12:03:24 +0800604 raise core.ExecutionFatalError(
605 '%s config %s: %s' % (sys.argv[0], opts.key, result))
Kuang-che Wu88518882017-09-22 16:57:25 +0800606
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:
Kuang-che Wu74768d32018-09-07 12:03:24 +0800628 raise core.ExecutionFatalError(
629 '%s config: unknown key: %r' % (sys.argv[0], opts.key))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800630
631 self.states.save()
632
633 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800634 if self.domain_cls.help:
635 description = self.domain_cls.help
636 else:
637 description = 'Bisector for %s' % self.domain_cls.__name__
638 description += textwrap.dedent('''
639 When running switcher and evaluator, it will set BISECT_REV environment
640 variable, indicates current rev to switch/evaluate.
641 ''')
642
Kuang-che Wu88875db2017-07-20 10:47:53 +0800643 parser = argparse.ArgumentParser(
644 prog=prog,
645 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800646 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800647 common.add_common_arguments(parser)
648 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800649 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800650 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800651 help='Directory to store sessions (default: %(default)r)')
652 parser.add_argument(
653 '--session',
654 default=DEFAULT_SESSION_NAME,
655 help='Session name (default: %(default)r)')
656 subparsers = parser.add_subparsers(
657 dest='command', title='commands', metavar='<command>')
658
659 parser_reset = subparsers.add_parser(
660 'reset', help='Reset bisect session and clean up saved result')
661 parser_reset.set_defaults(func=self.cmd_reset)
662
663 parser_init = subparsers.add_parser(
664 'init',
665 help='Initializes bisect session',
666 formatter_class=argparse.RawDescriptionHelpFormatter,
667 description=textwrap.dedent('''
668 Besides arguments for 'init' command, you also need to set 'switch'
669 and 'eval' command line via 'config' command.
670 $ bisector config switch <switch command and arguments>
671 $ bisector config eval <eval command and arguments>
672
673 The value of --noisy and --confidence could be changed by 'config'
674 command after 'init' as well.
675 '''))
676 parser_init.add_argument(
677 '--old',
678 required=True,
679 type=self.domain_cls.revtype,
680 help='Start of bisect range, which has old behavior')
681 parser_init.add_argument(
682 '--new',
683 required=True,
684 type=self.domain_cls.revtype,
685 help='End of bisect range, which has new behavior')
686 parser_init.add_argument(
687 '--noisy',
688 help='Enable noisy binary search and specify prior result. '
689 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
690 'and new fail rate increased to 2/3. '
691 'Skip if not flaky, say, "new=2/3" means old is always good.')
692 parser_init.add_argument(
693 '--confidence',
694 type=float,
695 default=DEFAULT_CONFIDENCE,
696 help='Confidence level (default: %(default)r)')
697 parser_init.set_defaults(func=self.cmd_init)
698 self.domain_cls.add_init_arguments(parser_init)
699
700 parser_config = subparsers.add_parser(
701 'config', help='Configures additional setting')
702 parser_config.add_argument(
703 'key',
704 choices=['switch', 'eval', 'confidence', 'noisy'],
705 metavar='key',
706 help='What config to change. choices=[%(choices)s]')
707 parser_config.add_argument(
708 'value', nargs=argparse.REMAINDER, help='New value')
709 parser_config.set_defaults(func=self.cmd_config)
710
711 parser_run = subparsers.add_parser(
712 'run',
713 help='Performs bisection',
714 formatter_class=argparse.RawDescriptionHelpFormatter,
715 description=textwrap.dedent('''
716 This command does switch and eval to determine candidates having old or
717 new behavior.
718
719 By default, it attempts to try versions in binary search manner until
720 found the first version having new behavior.
721
722 If version numbers are specified on command line, it just tries those
723 versions and record the result.
724
725 Example:
726 Bisect automatically.
727 $ %(prog)s
728
729 Switch and run version "2.13" and "2.14" and then stop.
730 $ %(prog)s 2.13 2.14
731 '''))
732 parser_run.add_argument(
733 '-1', '--once', action='store_true', help='Only run one step')
734 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800735 '--force',
736 action='store_true',
737 help="Run at least once even it's already done")
738 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800739 'revs',
740 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800741 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800742 help='revs to switch+eval; '
743 'default is calculating automatically and run until done')
744 parser_run.set_defaults(func=self.cmd_run)
745
746 parser_switch = subparsers.add_parser(
747 'switch', help='Switch to given rev without eval')
748 parser_switch.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800749 'rev', type=argtype_multiplexer(self.domain_cls.intra_revtype, 'next'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800750 parser_switch.set_defaults(func=self.cmd_switch)
751
752 parser_old = subparsers.add_parser(
753 'old', help='Tells bisect engine the said revs have "old" behavior')
754 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800755 'revs',
756 nargs='+',
757 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800758 parser_old.set_defaults(func=self.cmd_old)
759
760 parser_new = subparsers.add_parser(
761 'new', help='Tells bisect engine the said revs have "new" behavior')
762 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800763 'revs',
764 nargs='+',
765 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800766 parser_new.set_defaults(func=self.cmd_new)
767
768 parser_skip = subparsers.add_parser(
769 'skip', help='Tells bisect engine the said revs have "skip" behavior')
770 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800771 'revs',
772 nargs='+',
773 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800774 parser_skip.set_defaults(func=self.cmd_skip)
775
776 parser_view = subparsers.add_parser(
777 'view', help='Shows current progress and candidates')
778 parser_view.add_argument('--more', action='store_true')
779 parser_view.set_defaults(func=self.cmd_view)
780
781 parser_log = subparsers.add_parser(
782 'log', help='Prints what has been done so far')
783 parser_log.set_defaults(func=self.cmd_log)
784
785 parser_next = subparsers.add_parser(
786 'next', help='Prints next suggested rev to bisect')
787 parser_next.set_defaults(func=self.cmd_next)
788
789 return parser
790
791 def main(self, *args, **kwargs):
792 """Command line main function.
793
794 Args:
795 *args: Command line arguments.
796 **kwargs: additional non command line arguments passed by script code.
797 {
798 'prog': Program name; optional.
799 }
800 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800801 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800802 parser = self.create_argument_parser(kwargs.get('prog'))
803 opts = parser.parse_args(args or None)
804 common.config_logging(opts)
805
806 self._create_states(session=opts.session, session_base=opts.session_base)
807 if opts.command not in ('init', 'reset', 'config'):
808 self.states.load()
809 self.domain = self.domain_cls(self.states.config)
810 self.strategy = strategy.NoisyBinarySearch(
811 self.states.rev_info,
812 self.states.rev2idx(self.config['old']),
813 self.states.rev2idx(self.config['new']),
814 confidence=self.config['confidence'],
815 observation=self.config['noisy'])
816
817 return opts.func(opts)