blob: 2b83c6ba1fea14bcf4089090dea13df2b1656c98 [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 Wu1dc5bd72019-01-19 00:14:46 +0800349 def _format_status(self, status):
350 if status in ('old', 'new'):
Kuang-che Wub6756d42019-01-25 12:19:55 +0800351 return '%s behavior' % status
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800352 return status
353
Kuang-che Wu8b654092018-11-09 17:56:25 +0800354 def _add_sample(self, rev, status, **kwargs):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800355 idx = self.states.rev2idx(rev)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800356 self.states.add_sample(idx, status, **kwargs)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800357 self.strategy.update(idx, status)
358
359 def cmd_reset(self, _opts):
360 """Resets bisect session and clean up saved result."""
361 self.states.reset()
362
363 def cmd_init(self, opts):
364 """Initializes bisect session.
365
366 See init command's help message for more detail.
367 """
368 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800369 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800370 logger.debug('revlist %r', revlist)
371 if 'new' not in config:
372 config['new'] = opts.new
373 if 'old' not in config:
374 config['old'] = opts.old
375 assert len(revlist) >= 2
376 assert config['new'] in revlist
377 assert config['old'] in revlist
378 old_idx = revlist.index(config['old'])
379 new_idx = revlist.index(config['new'])
380 assert old_idx < new_idx
381
382 config.update(confidence=opts.confidence, noisy=opts.noisy)
383
384 self.states.init(config, revlist)
385 self.states.save()
386
387 def _switch_and_eval(self, rev, prev_rev=None):
388 """Switches and evaluates given version.
389
390 If current version equals to target, switch step will be skip.
391
392 Args:
393 rev: Target version.
394 prev_rev: Previous version.
395
396 Returns:
397 (step, status, values):
398 step: Last step executed ('switch' or 'eval').
399 status: Execution result ('old', 'new', or 'skip').
400 values: Collected values from eval step. None if last step is 'switch'.
401 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800402 if prev_rev != rev:
403 logger.debug('switch to rev=%s', rev)
404 t0 = time.time()
405 status = do_switch(self.config['switch'], self.domain, rev)
406 t1 = time.time()
407 if status == 'skip':
408 logger.debug('switch failed => skip')
409 return 'switch', status, None
410 self.states.data['stats']['switch_count'] += 1
411 self.states.data['stats']['switch_time'] += t1 - t0
412
413 logger.debug('eval rev=%s', rev)
414 t0 = time.time()
415 status, values = do_evaluate(self.config['eval'], self.domain, rev)
416 t1 = time.time()
417 if status == 'skip':
418 return 'eval', status, values
419 self.states.data['stats']['eval_count'] += 1
420 self.states.data['stats']['eval_time'] += t1 - t0
421
422 return 'eval', status, values
423
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800424 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800425 if opts.revs:
426 for rev in opts.revs:
427 idx = self.states.rev2idx(rev)
428 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
429 yield idx, rev
430 if opts.once:
431 break
432 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800433 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800434 idx = self.strategy.next_idx()
435 rev = self.states.idx2rev(idx)
436 logger.info('try idx=%d rev=%s', idx, rev)
437 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800438 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800439 if opts.once:
440 break
441
442 def cmd_run(self, opts):
443 """Performs bisection.
444
445 See run command's help message for more detail.
446
447 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800448 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800449 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800450 errors.UnableToProceed: Too many errors to narrow down further the
451 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800452 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800453 # Set dummy values in case exception raised before loop.
454 idx, rev = -1, None
455 try:
456 assert self.config.get('switch')
457 assert self.config.get('eval')
458 self.strategy.rebuild()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800459
Kuang-che Wu8b654092018-11-09 17:56:25 +0800460 prev_rev = None
461 force = opts.force
462 for idx, rev in self._next_idx_iter(opts, force):
463 if not force:
464 # Bail out if bisection range is unlikely true in order to prevent
465 # wasting time. This is necessary because some configurations (say,
466 # confidence) may be changed before cmd_run() and thus the bisection
467 # range becomes not acceptable.
468 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800469
Kuang-che Wu8b654092018-11-09 17:56:25 +0800470 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
Kuang-che Wu1dc5bd72019-01-19 00:14:46 +0800471 logger.info('rev=%s status => %s', rev, self._format_status(status))
Kuang-che Wu8b654092018-11-09 17:56:25 +0800472 force = False
473
474 self._add_sample(rev, status, values=values)
475 self.states.save()
476
477 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800478 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800479
Kuang-che Wu8b654092018-11-09 17:56:25 +0800480 if status == 'skip':
481 current_state = self.states.get(idx)
482 if current_state['skip'] > (
483 current_state['old'] + current_state['new'] + 1) * 5:
484 message = 'too much "skip" for rev=%r' % rev
485 raise errors.UnableToProceed(message)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800486
Kuang-che Wu8b654092018-11-09 17:56:25 +0800487 self.strategy.show_summary()
488
489 if step == 'switch' and status == 'skip':
490 # Previous switch failed and thus the current version is unknown. Set
491 # it None, so next switch operation won't be bypassed (due to
492 # optimization).
493 prev_rev = None
494 else:
495 prev_rev = rev
496
497 logger.info('done')
498 old_idx, new_idx = self.strategy.get_range()
499 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800500 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800501 except Exception as e:
502 exception_name = e.__class__.__name__
503 self.states.add_history(
504 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
505 self.states.save()
506 raise
507 finally:
508 if rev:
509 # progress so far
510 old_idx, new_idx = self.strategy.get_range()
511 self.states.add_history(
512 'range',
513 old=self.states.idx2rev(old_idx),
514 new=self.states.idx2rev(new_idx))
515 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800516
517 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800518 """Shows remaining candidates."""
Kuang-che Wu15874b62019-01-11 21:10:27 +0800519 try:
520 self.strategy.rebuild()
521 # Rebuild twice in order to re-estimate noise.
522 self.strategy.rebuild()
523 except errors.VerificationFailed:
524 # Do nothing, go ahead to show existing information anyway.
525 pass
Kuang-che Wue80bb872018-11-15 19:45:25 +0800526
527 old_idx, new_idx = self.strategy.get_range()
528 old, new = map(self.states.idx2rev, [old_idx, new_idx])
529 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
530 self.strategy.confidence / 10.0)
531 summary = {
532 'rev_info': [vars(info).copy() for info in self.states.rev_info],
533 'current_range': (old, new),
534 'highlight_range':
535 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
536 'prob':
537 self.strategy.prob,
538 'remaining_steps':
539 self.strategy.remaining_steps(),
540 }
541
542 if opts.verbose or opts.json:
543 interesting_indexes = set(range(len(summary['rev_info'])))
544 else:
545 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800546 if self.strategy.prob:
547 for i, p in enumerate(self.strategy.prob):
548 if p > 0.05:
549 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800550
551 self.domain.fill_candidate_summary(summary, interesting_indexes)
552
553 if opts.json:
554 print(json.dumps(summary, indent=2, sort_keys=True))
555 else:
556 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
557
558 def show_summary(self, summary, interesting_indexes, verbose=False):
559 old, new = summary['current_range']
560 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
561
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800562 for link in summary.get('links', []):
563 print('%s: %s' % (link['name'], link['url']))
564 if 'note' in link:
565 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800566
567 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800568 if summary.get('remaining_steps'):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800569 print('(roughly %d steps)' % summary['remaining_steps'])
570
571 for i, rev_info in enumerate(summary['rev_info']):
572 if (not verbose and not old_idx <= i <= new_idx and
573 not rev_info['result_counter']):
574 continue
575
576 detail = []
Kuang-che Wu05e416e2019-02-21 12:33:52 +0800577 if self.strategy.is_noisy() and summary['prob']:
Kuang-che Wua8c987f2019-01-18 14:26:43 +0800578 detail.append('%.4f%%' % (summary['prob'][i] * 100))
Kuang-che Wue80bb872018-11-15 19:45:25 +0800579 if rev_info['result_counter']:
580 detail.append(str(rev_info['result_counter']))
581 values = sorted(rev_info['values'])
582 if len(values) == 1:
583 detail.append('%.3f' % values[0])
584 elif len(values) > 1:
585 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
586 (len(values), sum(values) / len(values),
587 values[len(values) // 2], values[0], values[-1]))
588
589 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
590 if i in interesting_indexes:
591 if 'comment' in rev_info:
592 print('\t%s' % rev_info['comment'])
593 for action in rev_info.get('actions', []):
594 if 'text' in action:
595 print('\t%s' % action['text'])
596 if 'link' in action:
597 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800598
599 def current_status(self, session=None, session_base=None):
600 """Gets current bisect status.
601
602 Returns:
603 A dict describing current status. It contains following items:
604 inited: True iff the session file is initialized (init command has been
605 invoked). If not, below items are omitted.
606 old: Start of current estimated range.
607 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800608 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800609 estimated_noise: New estimated noise.
610 done: True if bisection is done, otherwise False.
611 """
612 self._create_states(session=session, session_base=session_base)
613 if self.states.load():
614 self.strategy = strategy.NoisyBinarySearch(
615 self.states.rev_info,
616 self.states.rev2idx(self.config['old']),
617 self.states.rev2idx(self.config['new']),
618 confidence=self.config['confidence'],
619 observation=self.config['noisy'])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800620 try:
621 self.strategy.rebuild()
622 except errors.VerificationFailed:
623 # Do nothing, go ahead to show existing information anyway.
624 pass
Kuang-che Wu88875db2017-07-20 10:47:53 +0800625 left, right = self.strategy.get_range()
626 estimated_noise = self.strategy.get_noise_observation()
627
628 result = dict(
629 inited=True,
630 old=self.states.idx2rev(left),
631 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800632 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800633 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800634 done=self.strategy.is_done())
635 else:
636 result = dict(inited=False)
637 return result
638
Kuang-che Wu8b654092018-11-09 17:56:25 +0800639 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800640 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800641 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800642 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800643 if opts.before and entry['timestamp'] >= opts.before:
644 continue
645 if opts.after and entry['timestamp'] <= opts.after:
646 continue
647 history.append(entry)
648
649 if opts.json:
650 print(json.dumps(history, indent=2))
651 return
652
653 for entry in history:
654 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
655 if entry.get('event', 'sample') == 'sample':
656 print('{datetime} {rev} {status} {values} {comment}'.format(
657 datetime=entry_time,
658 rev=entry['rev'],
659 status=entry['status'] + ('*%d' % entry['times']
660 if entry.get('times', 1) > 1 else ''),
661 values=entry.get('values', ''),
662 comment=entry.get('comment', '')))
663 else:
664 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800665
666 def cmd_next(self, _opts):
667 """Prints next suggested rev to bisect."""
668 self.strategy.rebuild()
669 if self.strategy.is_done():
670 print('done')
671 return
672
673 idx = self.strategy.next_idx()
674 rev = self.states.idx2rev(idx)
675 print(rev)
676
677 def cmd_switch(self, opts):
678 """Switches to given rev without eval."""
679 assert self.config.get('switch')
680
681 self.strategy.rebuild()
682
683 if opts.rev == 'next':
684 idx = self.strategy.next_idx()
685 rev = self.states.idx2rev(idx)
686 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800687 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800688 assert rev
689
690 logger.info('switch to %s', rev)
691 status = do_switch(self.config['switch'], self.domain, rev)
692 if status:
693 print('switch failed')
694
695 def _add_revs_status_helper(self, revs, status):
696 self.strategy.rebuild()
697 for rev, times in revs:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800698 self._add_sample(rev, status, times=times, comment='manual')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800699 self.states.save()
700
701 def cmd_new(self, opts):
702 """Tells bisect engine the said revs have "new" behavior."""
703 logger.info('set [%s] as new', opts.revs)
704 self._add_revs_status_helper(opts.revs, 'new')
705
706 def cmd_old(self, opts):
707 """Tells bisect engine the said revs have "old" behavior."""
708 logger.info('set [%s] as old', opts.revs)
709 self._add_revs_status_helper(opts.revs, 'old')
710
711 def cmd_skip(self, opts):
712 """Tells bisect engine the said revs have "skip" behavior."""
713 logger.info('set [%s] as skip', opts.revs)
714 self._add_revs_status_helper(opts.revs, 'skip')
715
716 def _create_states(self, session=None, session_base=None):
717 if not session:
718 session = DEFAULT_SESSION_NAME
719 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800720 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800721
722 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
723
724 if self.states:
725 assert self.states.session_file == session_file
726 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800727 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800728
729 def cmd_config(self, opts):
730 """Configures additional setting.
731
732 See config command's help message for more detail.
733 """
734 self.states.load()
735 self.domain = self.domain_cls(self.states.config)
736 if not opts.value:
737 print(self.states.config[opts.key])
738 return
739
740 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800741 result = check_executable(opts.value[0])
742 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800743 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800744
Kuang-che Wu88875db2017-07-20 10:47:53 +0800745 self.states.config[opts.key] = opts.value
746
747 elif opts.key == 'confidence':
748 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800749 raise errors.ArgumentError(
750 'confidence value',
751 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800752 try:
753 self.states.config[opts.key] = float(opts.value[0])
754 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800755 raise errors.ArgumentError('confidence value',
756 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800757
758 elif opts.key == 'noisy':
759 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800760 raise errors.ArgumentError(
761 'noisy value',
762 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800763 self.states.config[opts.key] = opts.value[0]
764
765 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800766 # unreachable
767 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800768
769 self.states.save()
770
771 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800772 if self.domain_cls.help:
773 description = self.domain_cls.help
774 else:
775 description = 'Bisector for %s' % self.domain_cls.__name__
776 description += textwrap.dedent('''
777 When running switcher and evaluator, it will set BISECT_REV environment
778 variable, indicates current rev to switch/evaluate.
779 ''')
780
Kuang-che Wu88875db2017-07-20 10:47:53 +0800781 parser = argparse.ArgumentParser(
782 prog=prog,
783 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800784 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800785 common.add_common_arguments(parser)
786 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800787 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800788 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800789 help='Directory to store sessions (default: %(default)r)')
790 parser.add_argument(
791 '--session',
792 default=DEFAULT_SESSION_NAME,
793 help='Session name (default: %(default)r)')
794 subparsers = parser.add_subparsers(
795 dest='command', title='commands', metavar='<command>')
796
797 parser_reset = subparsers.add_parser(
798 'reset', help='Reset bisect session and clean up saved result')
799 parser_reset.set_defaults(func=self.cmd_reset)
800
801 parser_init = subparsers.add_parser(
802 'init',
803 help='Initializes bisect session',
804 formatter_class=argparse.RawDescriptionHelpFormatter,
805 description=textwrap.dedent('''
806 Besides arguments for 'init' command, you also need to set 'switch'
807 and 'eval' command line via 'config' command.
808 $ bisector config switch <switch command and arguments>
809 $ bisector config eval <eval command and arguments>
810
811 The value of --noisy and --confidence could be changed by 'config'
812 command after 'init' as well.
813 '''))
814 parser_init.add_argument(
815 '--old',
816 required=True,
817 type=self.domain_cls.revtype,
818 help='Start of bisect range, which has old behavior')
819 parser_init.add_argument(
820 '--new',
821 required=True,
822 type=self.domain_cls.revtype,
823 help='End of bisect range, which has new behavior')
824 parser_init.add_argument(
825 '--noisy',
826 help='Enable noisy binary search and specify prior result. '
827 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
828 'and new fail rate increased to 2/3. '
829 'Skip if not flaky, say, "new=2/3" means old is always good.')
830 parser_init.add_argument(
831 '--confidence',
832 type=float,
833 default=DEFAULT_CONFIDENCE,
834 help='Confidence level (default: %(default)r)')
835 parser_init.set_defaults(func=self.cmd_init)
836 self.domain_cls.add_init_arguments(parser_init)
837
838 parser_config = subparsers.add_parser(
839 'config', help='Configures additional setting')
840 parser_config.add_argument(
841 'key',
842 choices=['switch', 'eval', 'confidence', 'noisy'],
843 metavar='key',
844 help='What config to change. choices=[%(choices)s]')
845 parser_config.add_argument(
846 'value', nargs=argparse.REMAINDER, help='New value')
847 parser_config.set_defaults(func=self.cmd_config)
848
849 parser_run = subparsers.add_parser(
850 'run',
851 help='Performs bisection',
852 formatter_class=argparse.RawDescriptionHelpFormatter,
853 description=textwrap.dedent('''
854 This command does switch and eval to determine candidates having old or
855 new behavior.
856
857 By default, it attempts to try versions in binary search manner until
858 found the first version having new behavior.
859
860 If version numbers are specified on command line, it just tries those
861 versions and record the result.
862
863 Example:
864 Bisect automatically.
865 $ %(prog)s
866
867 Switch and run version "2.13" and "2.14" and then stop.
868 $ %(prog)s 2.13 2.14
869 '''))
870 parser_run.add_argument(
871 '-1', '--once', action='store_true', help='Only run one step')
872 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800873 '--force',
874 action='store_true',
875 help="Run at least once even it's already done")
876 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800877 'revs',
878 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800879 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800880 help='revs to switch+eval; '
881 'default is calculating automatically and run until done')
882 parser_run.set_defaults(func=self.cmd_run)
883
884 parser_switch = subparsers.add_parser(
885 'switch', help='Switch to given rev without eval')
886 parser_switch.add_argument(
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800887 'rev',
888 type=argtype_multiplexer(self.domain_cls.intra_revtype,
889 argtype_re('next', 'next')))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800890 parser_switch.set_defaults(func=self.cmd_switch)
891
892 parser_old = subparsers.add_parser(
893 'old', help='Tells bisect engine the said revs have "old" behavior')
894 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800895 'revs',
896 nargs='+',
897 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800898 parser_old.set_defaults(func=self.cmd_old)
899
900 parser_new = subparsers.add_parser(
901 'new', help='Tells bisect engine the said revs have "new" behavior')
902 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800903 'revs',
904 nargs='+',
905 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800906 parser_new.set_defaults(func=self.cmd_new)
907
908 parser_skip = subparsers.add_parser(
909 'skip', help='Tells bisect engine the said revs have "skip" behavior')
910 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800911 'revs',
912 nargs='+',
913 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800914 parser_skip.set_defaults(func=self.cmd_skip)
915
916 parser_view = subparsers.add_parser(
917 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800918 parser_view.add_argument('--verbose', '-v', action='store_true')
919 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800920 parser_view.set_defaults(func=self.cmd_view)
921
922 parser_log = subparsers.add_parser(
923 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800924 parser_log.add_argument('--before', type=float)
925 parser_log.add_argument('--after', type=float)
926 parser_log.add_argument(
927 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800928 parser_log.set_defaults(func=self.cmd_log)
929
930 parser_next = subparsers.add_parser(
931 'next', help='Prints next suggested rev to bisect')
932 parser_next.set_defaults(func=self.cmd_next)
933
934 return parser
935
936 def main(self, *args, **kwargs):
937 """Command line main function.
938
939 Args:
940 *args: Command line arguments.
941 **kwargs: additional non command line arguments passed by script code.
942 {
943 'prog': Program name; optional.
944 }
945 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800946 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800947 parser = self.create_argument_parser(kwargs.get('prog'))
948 opts = parser.parse_args(args or None)
949 common.config_logging(opts)
950
951 self._create_states(session=opts.session, session_base=opts.session_base)
952 if opts.command not in ('init', 'reset', 'config'):
953 self.states.load()
954 self.domain = self.domain_cls(self.states.config)
955 self.strategy = strategy.NoisyBinarySearch(
956 self.states.rev_info,
957 self.states.rev2idx(self.config['old']),
958 self.states.rev2idx(self.config['new']),
959 confidence=self.config['confidence'],
960 observation=self.config['noisy'])
961
962 return opts.func(opts)