blob: 6a450e54f1ade8f97d83383c0250704df75f49a2 [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
78def argtype_multiplexer(*args):
79 r"""argtype multiplexer
80
81 This function takes a list of argtypes or regex patterns and creates a new
82 function matching them. Moreover, it gives error message with examples.
83
Kuang-che Wubaaa4532018-08-15 17:08:10 +080084 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +080085 >>> argtype = argtype_multiplexer(argtype_int, r'^r\d+$')
86 >>> argtype('123')
87 123
88 >>> argtype('r456')
89 r456
90 >>> argtype('hello')
91 ArgTypeError: Invalid argument (example value: 123, r\d+$)
92
93 Args:
94 *args: list of argtypes or regex pattern.
95
96 Returns:
97 A new argtype function which matches *args.
98 """
99
100 def validate(s):
101 examples = []
102 for t in args:
103 if isinstance(t, str):
104 if re.match(t, s):
105 return s
106 examples.append(t)
107 continue
108 try:
109 return t(s)
110 except ArgTypeError as e:
111 examples += e.example
112
113 msg = 'Invalid argument'
114 raise ArgTypeError(msg, examples)
115
116 return validate
117
118
119def argtype_multiplier(argtype):
120 """A new argtype that supports multiplier suffix of the given argtype.
121
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800122 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800123 Supports the given argtype accepting "foo" as argument, this function
124 generates a new argtype function which accepts argument like "foo*3".
125
126 Returns:
127 A new argtype function which returns (arg, times) where arg is accepted
128 by input `argtype` and times is repeating count. Note that if multiplier is
129 omitted, "times" is 1.
130 """
131
132 def helper(s):
133 m = re.match(r'^(.+)\*(\d+)$', s)
134 try:
135 if m:
136 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800137 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800138 except ArgTypeError as e:
139 # It should be okay to gives multiplier example only for the first one
140 # because it is just "example", no need to enumerate all possibilities.
141 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
142
143 return helper
144
145
146def argtype_dir_path(s):
147 """Validate argument is an existing directory.
148
149 Args:
150 s: string to validate.
151
152 Raises:
153 ArgTypeError if the path is not a directory.
154 """
155 if not os.path.exists(s):
156 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
157 if not os.path.isdir(s):
158 raise ArgTypeError('should be a directory', '/path/to/somewhere')
159
160 # Normalize, trim trailing path separators.
161 if len(s) > 1 and s[-1] == os.path.sep:
162 s = s[:-1]
163 return s
164
165
166def _collect_bisect_result_values(values, line):
167 """Collect bisect result values from output line.
168
169 Args:
170 values: Collected values are appending to this list.
171 line: One line of output string.
172 """
173 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
174 if m:
175 try:
176 values.extend(map(float, m.group(1).split()))
177 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800178 raise errors.InternalError(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800179 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
180
181
Kuang-che Wu88518882017-09-22 16:57:25 +0800182def check_executable(program):
183 """Checks whether a program is executable.
184
185 Args:
186 program: program path in question
187
188 Returns:
189 string as error message if `program` is not executable, or None otherwise.
190 It will return None if unable to determine as well.
191 """
192 returncode = util.call('which', program)
193 if returncode == 127: # No 'which' on this platform, skip the check.
194 return None
195 if returncode == 0: # is executable
196 return None
197
198 hint = ''
199 if not os.path.exists(program):
200 hint = 'Not in PATH?'
201 elif not os.path.isfile(program):
202 hint = 'Not a file'
203 elif not os.access(program, os.X_OK):
204 hint = 'Forgot to chmod +x?'
205 elif '/' not in program:
206 hint = 'Forgot to prepend "./" ?'
207 return '%r is not executable. %s' % (program, hint)
208
209
Kuang-che Wu88875db2017-07-20 10:47:53 +0800210def do_evaluate(evaluate_cmd, domain, rev):
211 """Invokes evaluator command.
212
213 The `evaluate_cmd` can get the target revision from the environment variable
214 named 'BISECT_REV'.
215
216 The result is determined according to the exit code of evaluator:
217 0: 'old'
218 1..124: 'new'
219 125: 'skip'
220 126, 127: fatal error
221 terminated by signal: fatal error
222
223 p.s. the definition of result is compatible with git-bisect(1).
224
225 It also extracts additional values from evaluate_cmd's stdout lines which
226 match the following format:
227 BISECT_RESULT_VALUES=<float>[, <float>]*
228
229 Args:
230 evaluate_cmd: evaluator command.
231 domain: a bisect_kit.core.Domain instance.
232 rev: version to evaluate.
233
234 Returns:
235 (result, values):
236 result is one of 'old', 'new', 'skip'.
237 values are additional collected values, like performance score.
238
239 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800240 errors.ExecutionFatalError if evaluator returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800241 """
242 env = os.environ.copy()
243 env['BISECT_REV'] = rev
244 domain.setenv(env, rev)
245
246 values = []
247 p = util.Popen(
248 evaluate_cmd,
249 env=env,
250 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
251 returncode = p.wait()
252 if returncode < 0 or returncode > 125:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800253 raise errors.ExecutionFatalError('eval failed: %s' % returncode)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800254
255 if returncode == 0:
256 return 'old', values
257 if returncode == 125:
258 return 'skip', values
259 return 'new', values
260
261
262def do_switch(switch_cmd, domain, rev):
263 """Invokes switcher command.
264
265 The `switch_cmd` can get the target revision from the environment variable
266 named 'BISECT_REV'.
267
268 The result is determined according to the exit code of switcher:
269 0: switch succeeded
270 1..125: 'skip'
271 126, 127: fatal error
272 terminated by signal: fatal error
273
274 In other words, any non-fatal errors are considered as 'skip'.
275
276 Args:
277 switch_cmd: switcher command.
278 domain: a bisect_kit.core.Domain instance.
279 rev: version to switch.
280
281 Returns:
282 None if switch successfully, 'skip' otherwise.
283
284 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800285 errors.ExecutionFatalError if switcher returned fatal error code.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800286 """
287 env = os.environ.copy()
288 env['BISECT_REV'] = rev
289 domain.setenv(env, rev)
290
291 returncode = util.call(*switch_cmd, env=env)
292 if returncode < 0 or returncode > 125:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800293 raise errors.ExecutionFatalError('switch failed: %s' % returncode)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800294
295 if returncode != 0:
296 return 'skip'
297 return None
298
299
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800300class BisectorCommandLine(object):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800301 """Bisector command line interface.
302
303 The typical usage pattern:
304
305 if __name__ == '__main__':
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800306 BisectorCommandLine(CustomDomain).main()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800307
308 where CustomDomain is a derived class of core.BisectDomain. See
Kuang-che Wu02170592018-07-09 21:42:44 +0800309 bisect_list.py as example.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800310
311 If you need to control the bisector using python code, the easier way is
312 passing command line arguments to main() function. For example,
313 bisector = Bisector(CustomDomain)
314 bisector.main('init', '--old', '123', '--new', '456')
315 bisector.main('config', 'switch', 'true')
316 bisector.main('config', 'eval', 'true')
317 bisector.main('run')
318 """
319
320 def __init__(self, domain_cls):
321 self.domain_cls = domain_cls
322 self.domain = None
323 self.states = None
324 self.strategy = None
325
326 @property
327 def config(self):
328 return self.states.config
329
Kuang-che Wu8b654092018-11-09 17:56:25 +0800330 def _add_sample(self, rev, status, **kwargs):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800331 idx = self.states.rev2idx(rev)
Kuang-che Wu8b654092018-11-09 17:56:25 +0800332 self.states.add_sample(idx, status, **kwargs)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800333 self.strategy.update(idx, status)
334
335 def cmd_reset(self, _opts):
336 """Resets bisect session and clean up saved result."""
337 self.states.reset()
338
339 def cmd_init(self, opts):
340 """Initializes bisect session.
341
342 See init command's help message for more detail.
343 """
344 config, revlist = self.domain_cls.init(opts)
Kuang-che Wu42551dd2018-01-16 17:27:20 +0800345 logger.info('found %d revs to bisect', len(revlist))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800346 logger.debug('revlist %r', revlist)
347 if 'new' not in config:
348 config['new'] = opts.new
349 if 'old' not in config:
350 config['old'] = opts.old
351 assert len(revlist) >= 2
352 assert config['new'] in revlist
353 assert config['old'] in revlist
354 old_idx = revlist.index(config['old'])
355 new_idx = revlist.index(config['new'])
356 assert old_idx < new_idx
357
358 config.update(confidence=opts.confidence, noisy=opts.noisy)
359
360 self.states.init(config, revlist)
361 self.states.save()
362
363 def _switch_and_eval(self, rev, prev_rev=None):
364 """Switches and evaluates given version.
365
366 If current version equals to target, switch step will be skip.
367
368 Args:
369 rev: Target version.
370 prev_rev: Previous version.
371
372 Returns:
373 (step, status, values):
374 step: Last step executed ('switch' or 'eval').
375 status: Execution result ('old', 'new', or 'skip').
376 values: Collected values from eval step. None if last step is 'switch'.
377 """
Kuang-che Wu88875db2017-07-20 10:47:53 +0800378 if prev_rev != rev:
379 logger.debug('switch to rev=%s', rev)
380 t0 = time.time()
381 status = do_switch(self.config['switch'], self.domain, rev)
382 t1 = time.time()
383 if status == 'skip':
384 logger.debug('switch failed => skip')
385 return 'switch', status, None
386 self.states.data['stats']['switch_count'] += 1
387 self.states.data['stats']['switch_time'] += t1 - t0
388
389 logger.debug('eval rev=%s', rev)
390 t0 = time.time()
391 status, values = do_evaluate(self.config['eval'], self.domain, rev)
392 t1 = time.time()
393 if status == 'skip':
394 return 'eval', status, values
395 self.states.data['stats']['eval_count'] += 1
396 self.states.data['stats']['eval_time'] += t1 - t0
397
398 return 'eval', status, values
399
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800400 def _next_idx_iter(self, opts, force):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800401 if opts.revs:
402 for rev in opts.revs:
403 idx = self.states.rev2idx(rev)
404 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
405 yield idx, rev
406 if opts.once:
407 break
408 else:
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800409 while force or not self.strategy.is_done():
Kuang-che Wu88875db2017-07-20 10:47:53 +0800410 idx = self.strategy.next_idx()
411 rev = self.states.idx2rev(idx)
412 logger.info('try idx=%d rev=%s', idx, rev)
413 yield idx, rev
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800414 force = False
Kuang-che Wu88875db2017-07-20 10:47:53 +0800415 if opts.once:
416 break
417
418 def cmd_run(self, opts):
419 """Performs bisection.
420
421 See run command's help message for more detail.
422
423 Raises:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800424 errors.VerificationFailed: The bisection range is verified false. We
Kuang-che Wu88875db2017-07-20 10:47:53 +0800425 expect 'old' at the first rev and 'new' at last rev.
Kuang-che Wue121fae2018-11-09 16:18:39 +0800426 errors.UnableToProceed: Too many errors to narrow down further the
427 bisection range.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800428 """
Kuang-che Wu8b654092018-11-09 17:56:25 +0800429 # Set dummy values in case exception raised before loop.
430 idx, rev = -1, None
431 try:
432 assert self.config.get('switch')
433 assert self.config.get('eval')
434 self.strategy.rebuild()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800435
Kuang-che Wu8b654092018-11-09 17:56:25 +0800436 prev_rev = None
437 force = opts.force
438 for idx, rev in self._next_idx_iter(opts, force):
439 if not force:
440 # Bail out if bisection range is unlikely true in order to prevent
441 # wasting time. This is necessary because some configurations (say,
442 # confidence) may be changed before cmd_run() and thus the bisection
443 # range becomes not acceptable.
444 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800445
Kuang-che Wu8b654092018-11-09 17:56:25 +0800446 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
447 logger.info('rev=%s => status %s', rev, status)
448 force = False
449
450 self._add_sample(rev, status, values=values)
451 self.states.save()
452
453 # Bail out if bisection range is unlikely true.
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800454 self.strategy.check_verification_range()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800455
Kuang-che Wu8b654092018-11-09 17:56:25 +0800456 if status == 'skip':
457 current_state = self.states.get(idx)
458 if current_state['skip'] > (
459 current_state['old'] + current_state['new'] + 1) * 5:
460 message = 'too much "skip" for rev=%r' % rev
461 raise errors.UnableToProceed(message)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800462
Kuang-che Wu8b654092018-11-09 17:56:25 +0800463 self.strategy.show_summary()
464
465 if step == 'switch' and status == 'skip':
466 # Previous switch failed and thus the current version is unknown. Set
467 # it None, so next switch operation won't be bypassed (due to
468 # optimization).
469 prev_rev = None
470 else:
471 prev_rev = rev
472
473 logger.info('done')
474 old_idx, new_idx = self.strategy.get_range()
475 self.states.add_history('done')
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800476 self.states.save()
Kuang-che Wu8b654092018-11-09 17:56:25 +0800477 except Exception as e:
478 exception_name = e.__class__.__name__
479 self.states.add_history(
480 'failed', text='%s: %s' % (exception_name, e), index=idx, rev=rev)
481 self.states.save()
482 raise
483 finally:
484 if rev:
485 # progress so far
486 old_idx, new_idx = self.strategy.get_range()
487 self.states.add_history(
488 'range',
489 old=self.states.idx2rev(old_idx),
490 new=self.states.idx2rev(new_idx))
491 self.states.save()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800492
493 def cmd_view(self, opts):
Kuang-che Wue80bb872018-11-15 19:45:25 +0800494 """Shows remaining candidates."""
Kuang-che Wu15874b62019-01-11 21:10:27 +0800495 try:
496 self.strategy.rebuild()
497 # Rebuild twice in order to re-estimate noise.
498 self.strategy.rebuild()
499 except errors.VerificationFailed:
500 # Do nothing, go ahead to show existing information anyway.
501 pass
Kuang-che Wue80bb872018-11-15 19:45:25 +0800502
503 old_idx, new_idx = self.strategy.get_range()
504 old, new = map(self.states.idx2rev, [old_idx, new_idx])
505 highlight_old_idx, highlight_new_idx = self.strategy.get_range(
506 self.strategy.confidence / 10.0)
507 summary = {
508 'rev_info': [vars(info).copy() for info in self.states.rev_info],
509 'current_range': (old, new),
510 'highlight_range':
511 map(self.states.idx2rev, [highlight_old_idx, highlight_new_idx]),
512 'prob':
513 self.strategy.prob,
514 'remaining_steps':
515 self.strategy.remaining_steps(),
516 }
517
518 if opts.verbose or opts.json:
519 interesting_indexes = set(range(len(summary['rev_info'])))
520 else:
521 interesting_indexes = set([old_idx, new_idx])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800522 if self.strategy.prob:
523 for i, p in enumerate(self.strategy.prob):
524 if p > 0.05:
525 interesting_indexes.add(i)
Kuang-che Wue80bb872018-11-15 19:45:25 +0800526
527 self.domain.fill_candidate_summary(summary, interesting_indexes)
528
529 if opts.json:
530 print(json.dumps(summary, indent=2, sort_keys=True))
531 else:
532 self.show_summary(summary, interesting_indexes, verbose=opts.verbose)
533
534 def show_summary(self, summary, interesting_indexes, verbose=False):
535 old, new = summary['current_range']
536 old_idx, new_idx = map(self.states.data['revlist'].index, [old, new])
537
Kuang-che Wuaccf9202019-01-04 15:40:42 +0800538 for link in summary.get('links', []):
539 print('%s: %s' % (link['name'], link['url']))
540 if 'note' in link:
541 print(link['note'])
Kuang-che Wue80bb872018-11-15 19:45:25 +0800542
543 print('Range: (%s, %s], %s revs left' % (old, new, (new_idx - old_idx)))
544 if 'remaining_steps' in summary:
545 print('(roughly %d steps)' % summary['remaining_steps'])
546
547 for i, rev_info in enumerate(summary['rev_info']):
548 if (not verbose and not old_idx <= i <= new_idx and
549 not rev_info['result_counter']):
550 continue
551
552 detail = []
553 if self.strategy.is_noisy():
554 detail.append('%.4f%%' % summary['prob'][i] * 100)
555 if rev_info['result_counter']:
556 detail.append(str(rev_info['result_counter']))
557 values = sorted(rev_info['values'])
558 if len(values) == 1:
559 detail.append('%.3f' % values[0])
560 elif len(values) > 1:
561 detail.append('n=%d,avg=%.3f,median=%.3f,min=%.3f,max=%.3f' %
562 (len(values), sum(values) / len(values),
563 values[len(values) // 2], values[0], values[-1]))
564
565 print('[%d] %s\t%s' % (i, rev_info['rev'], ' '.join(detail)))
566 if i in interesting_indexes:
567 if 'comment' in rev_info:
568 print('\t%s' % rev_info['comment'])
569 for action in rev_info.get('actions', []):
570 if 'text' in action:
571 print('\t%s' % action['text'])
572 if 'link' in action:
573 print('\t%s' % action['link'])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800574
575 def current_status(self, session=None, session_base=None):
576 """Gets current bisect status.
577
578 Returns:
579 A dict describing current status. It contains following items:
580 inited: True iff the session file is initialized (init command has been
581 invoked). If not, below items are omitted.
582 old: Start of current estimated range.
583 new: End of current estimated range.
Kuang-che Wu8b654092018-11-09 17:56:25 +0800584 verified: The bisect range is already verified.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800585 estimated_noise: New estimated noise.
586 done: True if bisection is done, otherwise False.
587 """
588 self._create_states(session=session, session_base=session_base)
589 if self.states.load():
590 self.strategy = strategy.NoisyBinarySearch(
591 self.states.rev_info,
592 self.states.rev2idx(self.config['old']),
593 self.states.rev2idx(self.config['new']),
594 confidence=self.config['confidence'],
595 observation=self.config['noisy'])
Kuang-che Wu15874b62019-01-11 21:10:27 +0800596 try:
597 self.strategy.rebuild()
598 except errors.VerificationFailed:
599 # Do nothing, go ahead to show existing information anyway.
600 pass
Kuang-che Wu88875db2017-07-20 10:47:53 +0800601 left, right = self.strategy.get_range()
602 estimated_noise = self.strategy.get_noise_observation()
603
604 result = dict(
605 inited=True,
606 old=self.states.idx2rev(left),
607 new=self.states.idx2rev(right),
Kuang-che Wu8b654092018-11-09 17:56:25 +0800608 verified=self.strategy.is_range_verified(),
Kuang-che Wudd7f6f02018-06-28 18:19:30 +0800609 estimated_noise=estimated_noise,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800610 done=self.strategy.is_done())
611 else:
612 result = dict(inited=False)
613 return result
614
Kuang-che Wu8b654092018-11-09 17:56:25 +0800615 def cmd_log(self, opts):
Kuang-che Wu88875db2017-07-20 10:47:53 +0800616 """Prints what has been done so far."""
Kuang-che Wu8b654092018-11-09 17:56:25 +0800617 history = []
Kuang-che Wu88875db2017-07-20 10:47:53 +0800618 for entry in self.states.data['history']:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800619 if opts.before and entry['timestamp'] >= opts.before:
620 continue
621 if opts.after and entry['timestamp'] <= opts.after:
622 continue
623 history.append(entry)
624
625 if opts.json:
626 print(json.dumps(history, indent=2))
627 return
628
629 for entry in history:
630 entry_time = datetime.datetime.fromtimestamp(int(entry['timestamp']))
631 if entry.get('event', 'sample') == 'sample':
632 print('{datetime} {rev} {status} {values} {comment}'.format(
633 datetime=entry_time,
634 rev=entry['rev'],
635 status=entry['status'] + ('*%d' % entry['times']
636 if entry.get('times', 1) > 1 else ''),
637 values=entry.get('values', ''),
638 comment=entry.get('comment', '')))
639 else:
640 print('%s %r' % (entry_time, entry))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800641
642 def cmd_next(self, _opts):
643 """Prints next suggested rev to bisect."""
644 self.strategy.rebuild()
645 if self.strategy.is_done():
646 print('done')
647 return
648
649 idx = self.strategy.next_idx()
650 rev = self.states.idx2rev(idx)
651 print(rev)
652
653 def cmd_switch(self, opts):
654 """Switches to given rev without eval."""
655 assert self.config.get('switch')
656
657 self.strategy.rebuild()
658
659 if opts.rev == 'next':
660 idx = self.strategy.next_idx()
661 rev = self.states.idx2rev(idx)
662 else:
Kuang-che Wu752228c2018-09-05 13:54:22 +0800663 rev = self.domain_cls.intra_revtype(opts.rev)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800664 assert rev
665
666 logger.info('switch to %s', rev)
667 status = do_switch(self.config['switch'], self.domain, rev)
668 if status:
669 print('switch failed')
670
671 def _add_revs_status_helper(self, revs, status):
672 self.strategy.rebuild()
673 for rev, times in revs:
Kuang-che Wu8b654092018-11-09 17:56:25 +0800674 self._add_sample(rev, status, times=times, comment='manual')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800675 self.states.save()
676
677 def cmd_new(self, opts):
678 """Tells bisect engine the said revs have "new" behavior."""
679 logger.info('set [%s] as new', opts.revs)
680 self._add_revs_status_helper(opts.revs, 'new')
681
682 def cmd_old(self, opts):
683 """Tells bisect engine the said revs have "old" behavior."""
684 logger.info('set [%s] as old', opts.revs)
685 self._add_revs_status_helper(opts.revs, 'old')
686
687 def cmd_skip(self, opts):
688 """Tells bisect engine the said revs have "skip" behavior."""
689 logger.info('set [%s] as skip', opts.revs)
690 self._add_revs_status_helper(opts.revs, 'skip')
691
692 def _create_states(self, session=None, session_base=None):
693 if not session:
694 session = DEFAULT_SESSION_NAME
695 if not session_base:
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800696 session_base = configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800697
698 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
699
700 if self.states:
701 assert self.states.session_file == session_file
702 else:
Kuang-che Wuc5781932018-10-05 00:30:19 +0800703 self.states = core.BisectStates(session_file)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800704
705 def cmd_config(self, opts):
706 """Configures additional setting.
707
708 See config command's help message for more detail.
709 """
710 self.states.load()
711 self.domain = self.domain_cls(self.states.config)
712 if not opts.value:
713 print(self.states.config[opts.key])
714 return
715
716 if opts.key in ['switch', 'eval']:
Kuang-che Wu88518882017-09-22 16:57:25 +0800717 result = check_executable(opts.value[0])
718 if result:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800719 raise errors.ArgumentError('%s command' % opts.key, result)
Kuang-che Wu88518882017-09-22 16:57:25 +0800720
Kuang-che Wu88875db2017-07-20 10:47:53 +0800721 self.states.config[opts.key] = opts.value
722
723 elif opts.key == 'confidence':
724 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800725 raise errors.ArgumentError(
726 'confidence value',
727 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800728 try:
729 self.states.config[opts.key] = float(opts.value[0])
730 except ValueError:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800731 raise errors.ArgumentError('confidence value',
732 'invalid float value: %r' % opts.value[0])
Kuang-che Wu88875db2017-07-20 10:47:53 +0800733
734 elif opts.key == 'noisy':
735 if len(opts.value) != 1:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800736 raise errors.ArgumentError(
737 'noisy value',
738 'expected 1 value, %d values given' % len(opts.value))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800739 self.states.config[opts.key] = opts.value[0]
740
741 else:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800742 # unreachable
743 assert 0
Kuang-che Wu88875db2017-07-20 10:47:53 +0800744
745 self.states.save()
746
747 def create_argument_parser(self, prog):
Kuang-che Wub2376262017-11-20 18:05:24 +0800748 if self.domain_cls.help:
749 description = self.domain_cls.help
750 else:
751 description = 'Bisector for %s' % self.domain_cls.__name__
752 description += textwrap.dedent('''
753 When running switcher and evaluator, it will set BISECT_REV environment
754 variable, indicates current rev to switch/evaluate.
755 ''')
756
Kuang-che Wu88875db2017-07-20 10:47:53 +0800757 parser = argparse.ArgumentParser(
758 prog=prog,
759 formatter_class=argparse.RawDescriptionHelpFormatter,
Kuang-che Wub2376262017-11-20 18:05:24 +0800760 description=description)
Kuang-che Wu385279d2017-09-27 14:48:28 +0800761 common.add_common_arguments(parser)
762 parser.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800763 '--session_base',
Kuang-che Wu41e8b592018-09-25 17:01:30 +0800764 default=configure.get('SESSION_BASE', common.DEFAULT_SESSION_BASE),
Kuang-che Wu88875db2017-07-20 10:47:53 +0800765 help='Directory to store sessions (default: %(default)r)')
766 parser.add_argument(
767 '--session',
768 default=DEFAULT_SESSION_NAME,
769 help='Session name (default: %(default)r)')
770 subparsers = parser.add_subparsers(
771 dest='command', title='commands', metavar='<command>')
772
773 parser_reset = subparsers.add_parser(
774 'reset', help='Reset bisect session and clean up saved result')
775 parser_reset.set_defaults(func=self.cmd_reset)
776
777 parser_init = subparsers.add_parser(
778 'init',
779 help='Initializes bisect session',
780 formatter_class=argparse.RawDescriptionHelpFormatter,
781 description=textwrap.dedent('''
782 Besides arguments for 'init' command, you also need to set 'switch'
783 and 'eval' command line via 'config' command.
784 $ bisector config switch <switch command and arguments>
785 $ bisector config eval <eval command and arguments>
786
787 The value of --noisy and --confidence could be changed by 'config'
788 command after 'init' as well.
789 '''))
790 parser_init.add_argument(
791 '--old',
792 required=True,
793 type=self.domain_cls.revtype,
794 help='Start of bisect range, which has old behavior')
795 parser_init.add_argument(
796 '--new',
797 required=True,
798 type=self.domain_cls.revtype,
799 help='End of bisect range, which has new behavior')
800 parser_init.add_argument(
801 '--noisy',
802 help='Enable noisy binary search and specify prior result. '
803 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
804 'and new fail rate increased to 2/3. '
805 'Skip if not flaky, say, "new=2/3" means old is always good.')
806 parser_init.add_argument(
807 '--confidence',
808 type=float,
809 default=DEFAULT_CONFIDENCE,
810 help='Confidence level (default: %(default)r)')
811 parser_init.set_defaults(func=self.cmd_init)
812 self.domain_cls.add_init_arguments(parser_init)
813
814 parser_config = subparsers.add_parser(
815 'config', help='Configures additional setting')
816 parser_config.add_argument(
817 'key',
818 choices=['switch', 'eval', 'confidence', 'noisy'],
819 metavar='key',
820 help='What config to change. choices=[%(choices)s]')
821 parser_config.add_argument(
822 'value', nargs=argparse.REMAINDER, help='New value')
823 parser_config.set_defaults(func=self.cmd_config)
824
825 parser_run = subparsers.add_parser(
826 'run',
827 help='Performs bisection',
828 formatter_class=argparse.RawDescriptionHelpFormatter,
829 description=textwrap.dedent('''
830 This command does switch and eval to determine candidates having old or
831 new behavior.
832
833 By default, it attempts to try versions in binary search manner until
834 found the first version having new behavior.
835
836 If version numbers are specified on command line, it just tries those
837 versions and record the result.
838
839 Example:
840 Bisect automatically.
841 $ %(prog)s
842
843 Switch and run version "2.13" and "2.14" and then stop.
844 $ %(prog)s 2.13 2.14
845 '''))
846 parser_run.add_argument(
847 '-1', '--once', action='store_true', help='Only run one step')
848 parser_run.add_argument(
Kuang-che Wu889f68e2018-10-29 14:12:13 +0800849 '--force',
850 action='store_true',
851 help="Run at least once even it's already done")
852 parser_run.add_argument(
Kuang-che Wu88875db2017-07-20 10:47:53 +0800853 'revs',
854 nargs='*',
Kuang-che Wu752228c2018-09-05 13:54:22 +0800855 type=self.domain_cls.intra_revtype,
Kuang-che Wu88875db2017-07-20 10:47:53 +0800856 help='revs to switch+eval; '
857 'default is calculating automatically and run until done')
858 parser_run.set_defaults(func=self.cmd_run)
859
860 parser_switch = subparsers.add_parser(
861 'switch', help='Switch to given rev without eval')
862 parser_switch.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800863 'rev', type=argtype_multiplexer(self.domain_cls.intra_revtype, 'next'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800864 parser_switch.set_defaults(func=self.cmd_switch)
865
866 parser_old = subparsers.add_parser(
867 'old', help='Tells bisect engine the said revs have "old" behavior')
868 parser_old.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800869 'revs',
870 nargs='+',
871 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800872 parser_old.set_defaults(func=self.cmd_old)
873
874 parser_new = subparsers.add_parser(
875 'new', help='Tells bisect engine the said revs have "new" behavior')
876 parser_new.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800877 'revs',
878 nargs='+',
879 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800880 parser_new.set_defaults(func=self.cmd_new)
881
882 parser_skip = subparsers.add_parser(
883 'skip', help='Tells bisect engine the said revs have "skip" behavior')
884 parser_skip.add_argument(
Kuang-che Wu752228c2018-09-05 13:54:22 +0800885 'revs',
886 nargs='+',
887 type=argtype_multiplier(self.domain_cls.intra_revtype))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800888 parser_skip.set_defaults(func=self.cmd_skip)
889
890 parser_view = subparsers.add_parser(
891 'view', help='Shows current progress and candidates')
Kuang-che Wue80bb872018-11-15 19:45:25 +0800892 parser_view.add_argument('--verbose', '-v', action='store_true')
893 parser_view.add_argument('--json', action='store_true')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800894 parser_view.set_defaults(func=self.cmd_view)
895
896 parser_log = subparsers.add_parser(
897 'log', help='Prints what has been done so far')
Kuang-che Wu8b654092018-11-09 17:56:25 +0800898 parser_log.add_argument('--before', type=float)
899 parser_log.add_argument('--after', type=float)
900 parser_log.add_argument(
901 '--json', action='store_true', help='Machine readable output')
Kuang-che Wu88875db2017-07-20 10:47:53 +0800902 parser_log.set_defaults(func=self.cmd_log)
903
904 parser_next = subparsers.add_parser(
905 'next', help='Prints next suggested rev to bisect')
906 parser_next.set_defaults(func=self.cmd_next)
907
908 return parser
909
910 def main(self, *args, **kwargs):
911 """Command line main function.
912
913 Args:
914 *args: Command line arguments.
915 **kwargs: additional non command line arguments passed by script code.
916 {
917 'prog': Program name; optional.
918 }
919 """
Kuang-che Wu385279d2017-09-27 14:48:28 +0800920 common.init()
Kuang-che Wu88875db2017-07-20 10:47:53 +0800921 parser = self.create_argument_parser(kwargs.get('prog'))
922 opts = parser.parse_args(args or None)
923 common.config_logging(opts)
924
925 self._create_states(session=opts.session, session_base=opts.session_base)
926 if opts.command not in ('init', 'reset', 'config'):
927 self.states.load()
928 self.domain = self.domain_cls(self.states.config)
929 self.strategy = strategy.NoisyBinarySearch(
930 self.states.rev_info,
931 self.states.rev2idx(self.config['old']),
932 self.states.rev2idx(self.config['new']),
933 confidence=self.config['confidence'],
934 observation=self.config['noisy'])
935
936 return opts.func(opts)