blob: 7064a2027e98b72eba8c8832136944da75ec7959 [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
Kuang-che Wu8b654092018-11-09 17:56:25 +080010import json
Kuang-che Wu88875db2017-07-20 10:47:53 +080011import logging
12import os
13import re
Kuang-che Wu88875db2017-07-20 10:47:53 +080014import 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
Kuang-che Wue121fae2018-11-09 16:18:39 +080020from bisect_kit import errors
Kuang-che Wu88875db2017-07-20 10:47:53 +080021from bisect_kit import strategy
22from bisect_kit import util
23
24logger = logging.getLogger(__name__)
25
Kuang-che Wu88875db2017-07-20 10:47:53 +080026DEFAULT_SESSION_NAME = 'default'
27DEFAULT_CONFIDENCE = 0.999
28
29
30class ArgTypeError(argparse.ArgumentTypeError):
31 """An error for argument validation failure.
32
33 This not only tells users the argument is wrong but also gives correct
34 example. The main purpose of this error is for argtype_multiplexer, which
35 cascades examples from multiple ArgTypeError.
36 """
37
38 def __init__(self, msg, example):
39 self.msg = msg
40 if isinstance(example, list):
41 self.example = example
42 else:
43 self.example = [example]
44 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
45 super(ArgTypeError, self).__init__(full_msg)
46
47
48def argtype_notempty(s):
49 """Validates argument is not an empty string.
50
51 Args:
52 s: string to validate.
53
54 Raises:
55 ArgTypeError if argument is empty string.
56 """
57 if not s:
58 msg = 'should not be empty'
59 raise ArgTypeError(msg, 'foo')
60 return s
61
62
63def argtype_int(s):
64 """Validate argument is a number.
65
66 Args:
67 s: string to validate.
68
69 Raises:
70 ArgTypeError if argument is not a number.
71 """
72 try:
73 return str(int(s))
74 except ValueError:
75 raise ArgTypeError('should be a number', '123')
76
77
Kuang-che Wu603cdad2019-01-18 21:32:55 +080078def argtype_re(pattern, example):
79 r"""Validate argument matches `pattern`.
80
81 Args:
82 pattern: regex pattern
83 example: example string which matches `pattern`
84
85 Returns:
86 A new argtype function which matches regex `pattern`
87 """
88 assert re.match(pattern, example)
89
90 def validate(s):
91 if re.match(pattern, s):
92 return s
93 if re.escape(pattern) == pattern:
94 raise ArgTypeError('should be "%s"' % pattern, pattern)
95 raise ArgTypeError('should match "%s"' % pattern,
96 '"%s" like %s' % (pattern, example))
97
98 return validate
99
100
Kuang-che Wu88875db2017-07-20 10:47:53 +0800101def argtype_multiplexer(*args):
102 r"""argtype multiplexer
103
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800104 This function takes a list of argtypes and creates a new function matching
105 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800106
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800107 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800108 >>> argtype = argtype_multiplexer(argtype_int,
109 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800110 >>> argtype('123')
111 123
112 >>> argtype('r456')
113 r456
114 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800115 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800116
117 Args:
118 *args: list of argtypes or regex pattern.
119
120 Returns:
121 A new argtype function which matches *args.
122 """
123
124 def validate(s):
125 examples = []
126 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800127 try:
128 return t(s)
129 except ArgTypeError as e:
130 examples += e.example
131
132 msg = 'Invalid argument'
133 raise ArgTypeError(msg, examples)
134
135 return validate
136
137
138def argtype_multiplier(argtype):
139 """A new argtype that supports multiplier suffix of the given argtype.
140
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800141 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800142 Supports the given argtype accepting "foo" as argument, this function
143 generates a new argtype function which accepts argument like "foo*3".
144
145 Returns:
146 A new argtype function which returns (arg, times) where arg is accepted
147 by input `argtype` and times is repeating count. Note that if multiplier is
148 omitted, "times" is 1.
149 """
150
151 def helper(s):
152 m = re.match(r'^(.+)\*(\d+)$', s)
153 try:
154 if m:
155 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800156 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800157 except ArgTypeError as e:
158 # It should be okay to gives multiplier example only for the first one
159 # because it is just "example", no need to enumerate all possibilities.
160 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
161
162 return helper
163
164
165def argtype_dir_path(s):
166 """Validate argument is an existing directory.
167
168 Args:
169 s: string to validate.
170
171 Raises:
172 ArgTypeError if the path is not a directory.
173 """
174 if not os.path.exists(s):
175 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
176 if not os.path.isdir(s):
177 raise ArgTypeError('should be a directory', '/path/to/somewhere')
178
179 # Normalize, trim trailing path separators.
180 if len(s) > 1 and s[-1] == os.path.sep:
181 s = s[:-1]
182 return s
183
184
185def _collect_bisect_result_values(values, line):
186 """Collect bisect result values from output line.
187
188 Args:
189 values: Collected values are appending to this list.
190 line: One line of output string.
191 """
192 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
193 if m:
194 try:
195 values.extend(map(float, m.group(1).split()))
196 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800197 raise errors.InternalError(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800198 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
199
200
Kuang-che Wu88518882017-09-22 16:57:25 +0800201def check_executable(program):
202 """Checks whether a program is executable.
203
204 Args:
205 program: program path in question
206
207 Returns:
208 string as error message if `program` is not executable, or None otherwise.
209 It will return None if unable to determine as well.
210 """
211 returncode = util.call('which', program)
212 if returncode == 127: # No 'which' on this platform, skip the check.
213 return None
214 if returncode == 0: # is executable
215 return None
216
217 hint = ''
218 if not os.path.exists(program):
219 hint = 'Not in PATH?'
220 elif not os.path.isfile(program):
221 hint = 'Not a file'
222 elif not os.access(program, os.X_OK):
223 hint = 'Forgot to chmod +x?'
224 elif '/' not in program:
225 hint = 'Forgot to prepend "./" ?'
226 return '%r is not executable. %s' % (program, hint)
227
228
Kuang-che Wu88875db2017-07-20 10:47:53 +0800229def do_evaluate(evaluate_cmd, domain, rev):
230 """Invokes evaluator command.
231
232 The `evaluate_cmd` can get the target revision from the environment variable
233 named 'BISECT_REV'.
234
235 The result is determined according to the exit code of evaluator:
236 0: 'old'
237 1..124: 'new'
238 125: 'skip'
239 126, 127: fatal error
240 terminated by signal: fatal error
241
242 p.s. the definition of result is compatible with git-bisect(1).
243
244 It also extracts additional values from evaluate_cmd's stdout lines which
245 match the following format:
246 BISECT_RESULT_VALUES=<float>[, <float>]*
247
248 Args:
249 evaluate_cmd: evaluator command.
250 domain: a bisect_kit.core.Domain instance.
251 rev: version to evaluate.
252
253 Returns:
254 (result, values):
255 result is one of 'old', 'new', 'skip'.
256 values are additional collected values, like performance score.
257
258 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800259 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800260 """
261 env = os.environ.copy()
262 env['BISECT_REV'] = rev
263 domain.setenv(env, rev)
264
265 values = []
266 p = util.Popen(
267 evaluate_cmd,
268 env=env,
269 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
270 returncode = p.wait()
271 if returncode < 0 or returncode > 125:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800272 raise errors.ExecutionFatalError('eval failed: %s' % returncode)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800273
274 if returncode == 0:
275 return 'old', values
276 if returncode == 125:
277 return 'skip', values
278 return 'new', values
279
280
281def do_switch(switch_cmd, domain, rev):
282 """Invokes switcher command.
283
284 The `switch_cmd` can get the target revision from the environment variable
285 named 'BISECT_REV'.
286
287 The result is determined according to the exit code of switcher:
288 0: switch succeeded
289 1..125: 'skip'
290 126, 127: fatal error
291 terminated by signal: fatal error
292
293 In other words, any non-fatal errors are considered as 'skip'.
294
295 Args:
296 switch_cmd: switcher command.
297 domain: a bisect_kit.core.Domain instance.
298 rev: version to switch.
299
300 Returns:
301 None if switch successfully, 'skip' otherwise.
302
303 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800304 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800305 """
306 env = os.environ.copy()
307 env['BISECT_REV'] = rev
308 domain.setenv(env, rev)
309
310 returncode = util.call(*switch_cmd, env=env)
311 if returncode < 0 or returncode > 125:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800312 raise errors.ExecutionFatalError('switch failed: %s' % returncode)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800313
314 if returncode != 0:
315 return 'skip'
316 return None
317
318
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800319class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800320 """Bisector command line interface.
321
322 The typical usage pattern:
323
324 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800325 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800326
327 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800328 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800329
330 If you need to control the bisector using python code, the easier way is
331 passing command line arguments to main() function. For example,
332 bisector = Bisector(CustomDomain)
333 bisector.main('init', '--old', '123', '--new', '456')
334 bisector.main('config', 'switch', 'true')
335 bisector.main('config', 'eval', 'true')
336 bisector.main('run')
337 """
338
339 def __init__(self, domain_cls):
340 self.domain_cls = domain_cls
341 self.domain = None
342 self.states = None
343 self.strategy = None
344
345 @property
346 def config(self):
347 return self.states.config
348
Kuang-che Wu8b654092018-11-09 17:56:25 +0800349 def _add_sample(self, rev, status, **kwargs):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800350 idx = self.states.rev2idx(rev)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800351 self.states.add_sample(idx, status, **kwargs)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800352 self.strategy.update(idx, status)
353
354 def cmd_reset(self, _opts):
355 """Resets bisect session and clean up saved result."""
356 self.states.reset()
357
358 def cmd_init(self, opts):
359 """Initializes bisect session.
360
361 See init command's help message for more detail.
362 """
363 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800364 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800365 logger.debug('revlist %r', revlist)
366 if 'new' not in config:
367 config['new'] = opts.new
368 if 'old' not in config:
369 config['old'] = opts.old
370 assert len(revlist) >= 2
371 assert config['new'] in revlist
372 assert config['old'] in revlist
373 old_idx = revlist.index(config['old'])
374 new_idx = revlist.index(config['new'])
375 assert old_idx < new_idx
376
377 config.update(confidence=opts.confidence, noisy=opts.noisy)
378
379 self.states.init(config, revlist)
380 self.states.save()
381
382 def _switch_and_eval(self, rev, prev_rev=None):
383 """Switches and evaluates given version.
384
385 If current version equals to target, switch step will be skip.
386
387 Args:
388 rev: Target version.
389 prev_rev: Previous version.
390
391 Returns:
392 (step, status, values):
393 step: Last step executed ('switch' or 'eval').
394 status: Execution result ('old', 'new', or 'skip').
395 values: Collected values from eval step. None if last step is 'switch'.
396 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800397 if prev_rev != rev:
398 logger.debug('switch to rev=%s', rev)
399 t0 = time.time()
400 status = do_switch(self.config['switch'], self.domain, rev)
401 t1 = time.time()
402 if status == 'skip':
403 logger.debug('switch failed => skip')
404 return 'switch', status, None
405 self.states.data['stats']['switch_count'] += 1
406 self.states.data['stats']['switch_time'] += t1 - t0
407
408 logger.debug('eval rev=%s', rev)
409 t0 = time.time()
410 status, values = do_evaluate(self.config['eval'], self.domain, rev)
411 t1 = time.time()
412 if status == 'skip':
413 return 'eval', status, values
414 self.states.data['stats']['eval_count'] += 1
415 self.states.data['stats']['eval_time'] += t1 - t0
416
417 return 'eval', status, values
418
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800419 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800420 if opts.revs:
421 for rev in opts.revs:
422 idx = self.states.rev2idx(rev)
423 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
424 yield idx, rev
425 if opts.once:
426 break
427 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800428 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800429 idx = self.strategy.next_idx()
430 rev = self.states.idx2rev(idx)
431 logger.info('try idx=%d rev=%s', idx, rev)
432 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800433 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800434 if opts.once:
435 break
436
437 def cmd_run(self, opts):
438 """Performs bisection.
439
440 See run command's help message for more detail.
441
442 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800443 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800444 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800445 errors.UnableToProceed: Too many errors to narrow down further the
446 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800447 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800448 # Set dummy values in case exception raised before loop.
449 idx, rev = -1, None
450 try:
451 assert self.config.get('switch')
452 assert self.config.get('eval')
453 self.strategy.rebuild()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800454
Kuang-che Wu8b654092018-11-09 17:56:25 +0800455 prev_rev = None
456 force = opts.force
457 for idx, rev in self._next_idx_iter(opts, force):
458 if not force:
459 # Bail out if bisection range is unlikely true in order to prevent
460 # wasting time. This is necessary because some configurations (say,
461 # confidence) may be changed before cmd_run() and thus the bisection
462 # range becomes not acceptable.
463 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800464
Kuang-che Wu8b654092018-11-09 17:56:25 +0800465 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
466 logger.info('rev=%s => status %s', rev, status)
467 force = False
468
469 self._add_sample(rev, status, values=values)
470 self.states.save()
471
472 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800473 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800474
Kuang-che Wu8b654092018-11-09 17:56:25 +0800475 if status == 'skip':
476 current_state = self.states.get(idx)
477 if current_state['skip'] > (
478 current_state['old'] + current_state['new'] + 1) * 5:
479 message = 'too much "skip" for rev=%r' % rev
480 raise errors.UnableToProceed(message)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800481
Kuang-che Wu8b654092018-11-09 17:56:25 +0800482 self.strategy.show_summary()
483
484 if step == 'switch' and status == 'skip':
485 # Previous switch failed and thus the current version is unknown. Set
486 # it None, so next switch operation won't be bypassed (due to
487 # optimization).
488 prev_rev = None
489 else:
490 prev_rev = rev
491
492 logger.info('done')
493 old_idx, new_idx = self.strategy.get_range()
494 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800495 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800496 except Exception as e:
497 exception_name = e.__class__.__name__
498 self.states.add_history(
499 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
500 self.states.save()
501 raise
502 finally:
503 if rev:
504 # progress so far
505 old_idx, new_idx = self.strategy.get_range()
506 self.states.add_history(
507 'range',
508 old=self.states.idx2rev(old_idx),
509 new=self.states.idx2rev(new_idx))
510 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800511
512 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800513 """Shows remaining candidates."""
Kuang-che Wu15874b62019-01-11 21:10:27 +0800514 try:
515 self.strategy.rebuild()
516 # Rebuild twice in order to re-estimate noise.
517 self.strategy.rebuild()
518 except errors.VerificationFailed:
519 # Do nothing, go ahead to show existing information anyway.
520 pass
Kuang-che Wue80bb872018-11-15 19:45:25 +0800521
522 old_idx, new_idx = self.strategy.get_range()
523 old, new = map(self.states.idx2rev, [old_idx, new_idx])
524 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
525 self.strategy.confidence / 10.0)
526 summary = {
527 'rev_info': [vars(info).copy() for info in self.states.rev_info],
528 'current_range': (old, new),
529 'highlight_range':
530 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
531 'prob':
532 self.strategy.prob,
533 'remaining_steps':
534 self.strategy.remaining_steps(),
535 }
536
537 if opts.verbose or opts.json:
538 interesting_indexes = set(range(len(summary['rev_info'])))
539 else:
540 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800541 if self.strategy.prob:
542 for i, p in enumerate(self.strategy.prob):
543 if p > 0.05:
544 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800545
546 self.domain.fill_candidate_summary(summary, interesting_indexes)
547
548 if opts.json:
549 print(json.dumps(summary, indent=2, sort_keys=True))
550 else:
551 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
552
553 def show_summary(self, summary, interesting_indexes, verbose=False):
554 old, new = summary['current_range']
555 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
556
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800557 for link in summary.get('links', []):
558 print('%s: %s' % (link['name'], link['url']))
559 if 'note' in link:
560 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800561
562 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800563 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800564 print('(roughly %d steps)' % summary['remaining_steps'])
565
566 for i, rev_info in enumerate(summary['rev_info']):
567 if (not verbose and not old_idx <= i <= new_idx and
568 not rev_info['result_counter']):
569 continue
570
571 detail = []
572 if self.strategy.is_noisy():
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800573 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800574 if rev_info['result_counter']:
575 detail.append(str(rev_info['result_counter']))
576 values = sorted(rev_info['values'])
577 if len(values) == 1:
578 detail.append('%.3f' % values[0])
579 elif len(values) > 1:
580 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
581 (len(values), sum(values) / len(values),
582 values[len(values) // 2], values[0], values[-1]))
583
584 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
585 if i in interesting_indexes:
586 if 'comment' in rev_info:
587 print('\t%s' % rev_info['comment'])
588 for action in rev_info.get('actions', []):
589 if 'text' in action:
590 print('\t%s' % action['text'])
591 if 'link' in action:
592 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800593
594 def current_status(self, session=None, session_base=None):
595 """Gets current bisect status.
596
597 Returns:
598 A dict describing current status. It contains following items:
599 inited: True iff the session file is initialized (init command has been
600 invoked). If not, below items are omitted.
601 old: Start of current estimated range.
602 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800603 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800604 estimated_noise: New estimated noise.
605 done: True if bisection is done, otherwise False.
606 """
607 self._create_states(session=session, session_base=session_base)
608 if self.states.load():
609 self.strategy = strategy.NoisyBinarySearch(
610 self.states.rev_info,
611 self.states.rev2idx(self.config['old']),
612 self.states.rev2idx(self.config['new']),
613 confidence=self.config['confidence'],
614 observation=self.config['noisy'])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800615 try:
616 self.strategy.rebuild()
617 except errors.VerificationFailed:
618 # Do nothing, go ahead to show existing information anyway.
619 pass
Kuang-che Wu88875db2017-07-20 10:47:53 +0800620 left, right = self.strategy.get_range()
621 estimated_noise = self.strategy.get_noise_observation()
622
623 result = dict(
624 inited=True,
625 old=self.states.idx2rev(left),
626 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800627 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800628 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800629 done=self.strategy.is_done())
630 else:
631 result = dict(inited=False)
632 return result
633
Kuang-che Wu8b654092018-11-09 17:56:25 +0800634 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800635 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800636 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800637 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800638 if opts.before and entry['timestamp'] >= opts.before:
639 continue
640 if opts.after and entry['timestamp'] <= opts.after:
641 continue
642 history.append(entry)
643
644 if opts.json:
645 print(json.dumps(history, indent=2))
646 return
647
648 for entry in history:
649 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
650 if entry.get('event', 'sample') == 'sample':
651 print('{datetime} {rev} {status} {values} {comment}'.format(
652 datetime=entry_time,
653 rev=entry['rev'],
654 status=entry['status'] + ('*%d' % entry['times']
655 if entry.get('times', 1) > 1 else ''),
656 values=entry.get('values', ''),
657 comment=entry.get('comment', '')))
658 else:
659 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800660
661 def cmd_next(self, _opts):
662 """Prints next suggested rev to bisect."""
663 self.strategy.rebuild()
664 if self.strategy.is_done():
665 print('done')
666 return
667
668 idx = self.strategy.next_idx()
669 rev = self.states.idx2rev(idx)
670 print(rev)
671
672 def cmd_switch(self, opts):
673 """Switches to given rev without eval."""
674 assert self.config.get('switch')
675
676 self.strategy.rebuild()
677
678 if opts.rev == 'next':
679 idx = self.strategy.next_idx()
680 rev = self.states.idx2rev(idx)
681 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800682 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800683 assert rev
684
685 logger.info('switch to %s', rev)
686 status = do_switch(self.config['switch'], self.domain, rev)
687 if status:
688 print('switch failed')
689
690 def _add_revs_status_helper(self, revs, status):
691 self.strategy.rebuild()
692 for rev, times in revs:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800693 self._add_sample(rev, status, times=times, comment='manual')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800694 self.states.save()
695
696 def cmd_new(self, opts):
697 """Tells bisect engine the said revs have "new" behavior."""
698 logger.info('set [%s] as new', opts.revs)
699 self._add_revs_status_helper(opts.revs, 'new')
700
701 def cmd_old(self, opts):
702 """Tells bisect engine the said revs have "old" behavior."""
703 logger.info('set [%s] as old', opts.revs)
704 self._add_revs_status_helper(opts.revs, 'old')
705
706 def cmd_skip(self, opts):
707 """Tells bisect engine the said revs have "skip" behavior."""
708 logger.info('set [%s] as skip', opts.revs)
709 self._add_revs_status_helper(opts.revs, 'skip')
710
711 def _create_states(self, session=None, session_base=None):
712 if not session:
713 session = DEFAULT_SESSION_NAME
714 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800715 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800716
717 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
718
719 if self.states:
720 assert self.states.session_file == session_file
721 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800722 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800723
724 def cmd_config(self, opts):
725 """Configures additional setting.
726
727 See config command's help message for more detail.
728 """
729 self.states.load()
730 self.domain = self.domain_cls(self.states.config)
731 if not opts.value:
732 print(self.states.config[opts.key])
733 return
734
735 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800736 result = check_executable(opts.value[0])
737 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800738 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800739
Kuang-che Wu88875db2017-07-20 10:47:53 +0800740 self.states.config[opts.key] = opts.value
741
742 elif opts.key == 'confidence':
743 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800744 raise errors.ArgumentError(
745 'confidence value',
746 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800747 try:
748 self.states.config[opts.key] = float(opts.value[0])
749 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800750 raise errors.ArgumentError('confidence value',
751 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800752
753 elif opts.key == 'noisy':
754 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800755 raise errors.ArgumentError(
756 'noisy value',
757 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800758 self.states.config[opts.key] = opts.value[0]
759
760 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800761 # unreachable
762 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800763
764 self.states.save()
765
766 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800767 if self.domain_cls.help:
768 description = self.domain_cls.help
769 else:
770 description = 'Bisector for %s' % self.domain_cls.__name__
771 description += textwrap.dedent('''
772 When running switcher and evaluator, it will set BISECT_REV environment
773 variable, indicates current rev to switch/evaluate.
774 ''')
775
Kuang-che Wu88875db2017-07-20 10:47:53 +0800776 parser = argparse.ArgumentParser(
777 prog=prog,
778 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800779 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800780 common.add_common_arguments(parser)
781 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800782 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800783 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800784 help='Directory to store sessions (default: %(default)r)')
785 parser.add_argument(
786 '--session',
787 default=DEFAULT_SESSION_NAME,
788 help='Session name (default: %(default)r)')
789 subparsers = parser.add_subparsers(
790 dest='command', title='commands', metavar='<command>')
791
792 parser_reset = subparsers.add_parser(
793 'reset', help='Reset bisect session and clean up saved result')
794 parser_reset.set_defaults(func=self.cmd_reset)
795
796 parser_init = subparsers.add_parser(
797 'init',
798 help='Initializes bisect session',
799 formatter_class=argparse.RawDescriptionHelpFormatter,
800 description=textwrap.dedent('''
801 Besides arguments for 'init' command, you also need to set 'switch'
802 and 'eval' command line via 'config' command.
803 $ bisector config switch <switch command and arguments>
804 $ bisector config eval <eval command and arguments>
805
806 The value of --noisy and --confidence could be changed by 'config'
807 command after 'init' as well.
808 '''))
809 parser_init.add_argument(
810 '--old',
811 required=True,
812 type=self.domain_cls.revtype,
813 help='Start of bisect range, which has old behavior')
814 parser_init.add_argument(
815 '--new',
816 required=True,
817 type=self.domain_cls.revtype,
818 help='End of bisect range, which has new behavior')
819 parser_init.add_argument(
820 '--noisy',
821 help='Enable noisy binary search and specify prior result. '
822 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
823 'and new fail rate increased to 2/3. '
824 'Skip if not flaky, say, "new=2/3" means old is always good.')
825 parser_init.add_argument(
826 '--confidence',
827 type=float,
828 default=DEFAULT_CONFIDENCE,
829 help='Confidence level (default: %(default)r)')
830 parser_init.set_defaults(func=self.cmd_init)
831 self.domain_cls.add_init_arguments(parser_init)
832
833 parser_config = subparsers.add_parser(
834 'config', help='Configures additional setting')
835 parser_config.add_argument(
836 'key',
837 choices=['switch', 'eval', 'confidence', 'noisy'],
838 metavar='key',
839 help='What config to change. choices=[%(choices)s]')
840 parser_config.add_argument(
841 'value', nargs=argparse.REMAINDER, help='New value')
842 parser_config.set_defaults(func=self.cmd_config)
843
844 parser_run = subparsers.add_parser(
845 'run',
846 help='Performs bisection',
847 formatter_class=argparse.RawDescriptionHelpFormatter,
848 description=textwrap.dedent('''
849 This command does switch and eval to determine candidates having old or
850 new behavior.
851
852 By default, it attempts to try versions in binary search manner until
853 found the first version having new behavior.
854
855 If version numbers are specified on command line, it just tries those
856 versions and record the result.
857
858 Example:
859 Bisect automatically.
860 $ %(prog)s
861
862 Switch and run version "2.13" and "2.14" and then stop.
863 $ %(prog)s 2.13 2.14
864 '''))
865 parser_run.add_argument(
866 '-1', '--once', action='store_true', help='Only run one step')
867 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800868 '--force',
869 action='store_true',
870 help="Run at least once even it's already done")
871 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800872 'revs',
873 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800874 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800875 help='revs to switch+eval; '
876 'default is calculating automatically and run until done')
877 parser_run.set_defaults(func=self.cmd_run)
878
879 parser_switch = subparsers.add_parser(
880 'switch', help='Switch to given rev without eval')
881 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800882 'rev',
883 type=argtype_multiplexer(self.domain_cls.intra_revtype,
884 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800885 parser_switch.set_defaults(func=self.cmd_switch)
886
887 parser_old = subparsers.add_parser(
888 'old', help='Tells bisect engine the said revs have "old" behavior')
889 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800890 'revs',
891 nargs='+',
892 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800893 parser_old.set_defaults(func=self.cmd_old)
894
895 parser_new = subparsers.add_parser(
896 'new', help='Tells bisect engine the said revs have "new" behavior')
897 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800898 'revs',
899 nargs='+',
900 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800901 parser_new.set_defaults(func=self.cmd_new)
902
903 parser_skip = subparsers.add_parser(
904 'skip', help='Tells bisect engine the said revs have "skip" behavior')
905 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800906 'revs',
907 nargs='+',
908 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800909 parser_skip.set_defaults(func=self.cmd_skip)
910
911 parser_view = subparsers.add_parser(
912 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800913 parser_view.add_argument('--verbose', '-v', action='store_true')
914 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800915 parser_view.set_defaults(func=self.cmd_view)
916
917 parser_log = subparsers.add_parser(
918 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800919 parser_log.add_argument('--before', type=float)
920 parser_log.add_argument('--after', type=float)
921 parser_log.add_argument(
922 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800923 parser_log.set_defaults(func=self.cmd_log)
924
925 parser_next = subparsers.add_parser(
926 'next', help='Prints next suggested rev to bisect')
927 parser_next.set_defaults(func=self.cmd_next)
928
929 return parser
930
931 def main(self, *args, **kwargs):
932 """Command line main function.
933
934 Args:
935 *args: Command line arguments.
936 **kwargs: additional non command line arguments passed by script code.
937 {
938 'prog': Program name; optional.
939 }
940 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800941 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800942 parser = self.create_argument_parser(kwargs.get('prog'))
943 opts = parser.parse_args(args or None)
944 common.config_logging(opts)
945
946 self._create_states(session=opts.session, session_base=opts.session_base)
947 if opts.command not in ('init', 'reset', 'config'):
948 self.states.load()
949 self.domain = self.domain_cls(self.states.config)
950 self.strategy = strategy.NoisyBinarySearch(
951 self.states.rev_info,
952 self.states.rev2idx(self.config['old']),
953 self.states.rev2idx(self.config['new']),
954 confidence=self.config['confidence'],
955 observation=self.config['noisy'])
956
957 return opts.func(opts)