blob: c422632040fbe8dd27d3c0a8b072ec437e955b46 [file] [log] [blame]
Kuang-che Wu88875db2017-07-20 10:47:53 +08001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Bisect command line interface."""
5
6from __future__ import print_function
7import argparse
8import datetime
9import logging
10import os
11import re
12import sys
13import textwrap
14import time
15
16from bisect_kit import common
17from bisect_kit import core
18from bisect_kit import strategy
19from bisect_kit import util
20
21logger = logging.getLogger(__name__)
22
23DEFAULT_SESSION_BASE = 'bisect.sessions'
24DEFAULT_SESSION_NAME = 'default'
25DEFAULT_CONFIDENCE = 0.999
26
27
28class ArgTypeError(argparse.ArgumentTypeError):
29 """An error for argument validation failure.
30
31 This not only tells users the argument is wrong but also gives correct
32 example. The main purpose of this error is for argtype_multiplexer, which
33 cascades examples from multiple ArgTypeError.
34 """
35
36 def __init__(self, msg, example):
37 self.msg = msg
38 if isinstance(example, list):
39 self.example = example
40 else:
41 self.example = [example]
42 full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example))
43 super(ArgTypeError, self).__init__(full_msg)
44
45
46def argtype_notempty(s):
47 """Validates argument is not an empty string.
48
49 Args:
50 s: string to validate.
51
52 Raises:
53 ArgTypeError if argument is empty string.
54 """
55 if not s:
56 msg = 'should not be empty'
57 raise ArgTypeError(msg, 'foo')
58 return s
59
60
61def argtype_int(s):
62 """Validate argument is a number.
63
64 Args:
65 s: string to validate.
66
67 Raises:
68 ArgTypeError if argument is not a number.
69 """
70 try:
71 return str(int(s))
72 except ValueError:
73 raise ArgTypeError('should be a number', '123')
74
75
76def argtype_multiplexer(*args):
77 r"""argtype multiplexer
78
79 This function takes a list of argtypes or regex patterns and creates a new
80 function matching them. Moreover, it gives error message with examples.
81
82 Example:
83 >>> argtype = argtype_multiplexer(argtype_int, r'^r\d+$')
84 >>> argtype('123')
85 123
86 >>> argtype('r456')
87 r456
88 >>> argtype('hello')
89 ArgTypeError: Invalid argument (example value: 123, r\d+$)
90
91 Args:
92 *args: list of argtypes or regex pattern.
93
94 Returns:
95 A new argtype function which matches *args.
96 """
97
98 def validate(s):
99 examples = []
100 for t in args:
101 if isinstance(t, str):
102 if re.match(t, s):
103 return s
104 examples.append(t)
105 continue
106 try:
107 return t(s)
108 except ArgTypeError as e:
109 examples += e.example
110
111 msg = 'Invalid argument'
112 raise ArgTypeError(msg, examples)
113
114 return validate
115
116
117def argtype_multiplier(argtype):
118 """A new argtype that supports multiplier suffix of the given argtype.
119
120 Example:
121 Supports the given argtype accepting "foo" as argument, this function
122 generates a new argtype function which accepts argument like "foo*3".
123
124 Returns:
125 A new argtype function which returns (arg, times) where arg is accepted
126 by input `argtype` and times is repeating count. Note that if multiplier is
127 omitted, "times" is 1.
128 """
129
130 def helper(s):
131 m = re.match(r'^(.+)\*(\d+)$', s)
132 try:
133 if m:
134 return argtype(m.group(1)), int(m.group(2))
135 else:
136 return argtype(s), 1
137 except ArgTypeError as e:
138 # It should be okay to gives multiplier example only for the first one
139 # because it is just "example", no need to enumerate all possibilities.
140 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3'])
141
142 return helper
143
144
145def argtype_dir_path(s):
146 """Validate argument is an existing directory.
147
148 Args:
149 s: string to validate.
150
151 Raises:
152 ArgTypeError if the path is not a directory.
153 """
154 if not os.path.exists(s):
155 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
156 if not os.path.isdir(s):
157 raise ArgTypeError('should be a directory', '/path/to/somewhere')
158
159 # Normalize, trim trailing path separators.
160 if len(s) > 1 and s[-1] == os.path.sep:
161 s = s[:-1]
162 return s
163
164
165def _collect_bisect_result_values(values, line):
166 """Collect bisect result values from output line.
167
168 Args:
169 values: Collected values are appending to this list.
170 line: One line of output string.
171 """
172 m = re.match(r'^BISECT_RESULT_VALUES=(.+)', line)
173 if m:
174 try:
175 values.extend(map(float, m.group(1).split()))
176 except ValueError:
177 raise core.ExecutionFatalError(
178 'BISECT_RESULT_VALUES should be list of floats: %r' % m.group(1))
179
180
181def do_evaluate(evaluate_cmd, domain, rev):
182 """Invokes evaluator command.
183
184 The `evaluate_cmd` can get the target revision from the environment variable
185 named 'BISECT_REV'.
186
187 The result is determined according to the exit code of evaluator:
188 0: 'old'
189 1..124: 'new'
190 125: 'skip'
191 126, 127: fatal error
192 terminated by signal: fatal error
193
194 p.s. the definition of result is compatible with git-bisect(1).
195
196 It also extracts additional values from evaluate_cmd's stdout lines which
197 match the following format:
198 BISECT_RESULT_VALUES=<float>[, <float>]*
199
200 Args:
201 evaluate_cmd: evaluator command.
202 domain: a bisect_kit.core.Domain instance.
203 rev: version to evaluate.
204
205 Returns:
206 (result, values):
207 result is one of 'old', 'new', 'skip'.
208 values are additional collected values, like performance score.
209
210 Raises:
211 core.ExecutionFatalError if evaluator returned fatal error code.
212 """
213 env = os.environ.copy()
214 env['BISECT_REV'] = rev
215 domain.setenv(env, rev)
216
217 values = []
218 p = util.Popen(
219 evaluate_cmd,
220 env=env,
221 stdout_callback=lambda line: _collect_bisect_result_values(values, line))
222 returncode = p.wait()
223 if returncode < 0 or returncode > 125:
224 raise core.ExecutionFatalError(str(returncode))
225
226 if returncode == 0:
227 return 'old', values
228 if returncode == 125:
229 return 'skip', values
230 return 'new', values
231
232
233def do_switch(switch_cmd, domain, rev):
234 """Invokes switcher command.
235
236 The `switch_cmd` can get the target revision from the environment variable
237 named 'BISECT_REV'.
238
239 The result is determined according to the exit code of switcher:
240 0: switch succeeded
241 1..125: 'skip'
242 126, 127: fatal error
243 terminated by signal: fatal error
244
245 In other words, any non-fatal errors are considered as 'skip'.
246
247 Args:
248 switch_cmd: switcher command.
249 domain: a bisect_kit.core.Domain instance.
250 rev: version to switch.
251
252 Returns:
253 None if switch successfully, 'skip' otherwise.
254
255 Raises:
256 core.ExecutionFatalError if switcher returned fatal error code.
257 """
258 env = os.environ.copy()
259 env['BISECT_REV'] = rev
260 domain.setenv(env, rev)
261
262 returncode = util.call(*switch_cmd, env=env)
263 if returncode < 0 or returncode > 125:
264 raise core.ExecutionFatalError(str(returncode))
265
266 if returncode != 0:
267 return 'skip'
268 return None
269
270
271# This is not an python "interface". pylint: disable=interface-not-implemented
272class BisectorCommandLineInterface(object):
273 """Bisector command line interface.
274
275 The typical usage pattern:
276
277 if __name__ == '__main__':
278 BisectorCommandLineInterface(CustomDomain).main()
279
280 where CustomDomain is a derived class of core.BisectDomain. See
281 bisect-list.py as example.
282
283 If you need to control the bisector using python code, the easier way is
284 passing command line arguments to main() function. For example,
285 bisector = Bisector(CustomDomain)
286 bisector.main('init', '--old', '123', '--new', '456')
287 bisector.main('config', 'switch', 'true')
288 bisector.main('config', 'eval', 'true')
289 bisector.main('run')
290 """
291
292 def __init__(self, domain_cls):
293 self.domain_cls = domain_cls
294 self.domain = None
295 self.states = None
296 self.strategy = None
297
298 @property
299 def config(self):
300 return self.states.config
301
302 def _add_status(self, rev, status, **kwargs):
303 idx = self.states.rev2idx(rev)
304 self.states.add_status(idx, status, **kwargs)
305 self.strategy.update(idx, status)
306
307 def cmd_reset(self, _opts):
308 """Resets bisect session and clean up saved result."""
309 self.states.reset()
310
311 def cmd_init(self, opts):
312 """Initializes bisect session.
313
314 See init command's help message for more detail.
315 """
316 config, revlist = self.domain_cls.init(opts)
317 logger.debug('revlist %r', revlist)
318 if 'new' not in config:
319 config['new'] = opts.new
320 if 'old' not in config:
321 config['old'] = opts.old
322 assert len(revlist) >= 2
323 assert config['new'] in revlist
324 assert config['old'] in revlist
325 old_idx = revlist.index(config['old'])
326 new_idx = revlist.index(config['new'])
327 assert old_idx < new_idx
328
329 config.update(confidence=opts.confidence, noisy=opts.noisy)
330
331 self.states.init(config, revlist)
332 self.states.save()
333
334 def _switch_and_eval(self, rev, prev_rev=None):
335 """Switches and evaluates given version.
336
337 If current version equals to target, switch step will be skip.
338
339 Args:
340 rev: Target version.
341 prev_rev: Previous version.
342
343 Returns:
344 (step, status, values):
345 step: Last step executed ('switch' or 'eval').
346 status: Execution result ('old', 'new', or 'skip').
347 values: Collected values from eval step. None if last step is 'switch'.
348 """
349 idx = self.states.rev2idx(rev)
350 if prev_rev != rev:
351 logger.debug('switch to rev=%s', rev)
352 t0 = time.time()
353 status = do_switch(self.config['switch'], self.domain, rev)
354 t1 = time.time()
355 if status == 'skip':
356 logger.debug('switch failed => skip')
357 return 'switch', status, None
358 self.states.data['stats']['switch_count'] += 1
359 self.states.data['stats']['switch_time'] += t1 - t0
360
361 logger.debug('eval rev=%s', rev)
362 t0 = time.time()
363 status, values = do_evaluate(self.config['eval'], self.domain, rev)
364 t1 = time.time()
365 if status == 'skip':
366 return 'eval', status, values
367 self.states.data['stats']['eval_count'] += 1
368 self.states.data['stats']['eval_time'] += t1 - t0
369
370 return 'eval', status, values
371
372 def _next_idx_iter(self, opts):
373 if opts.revs:
374 for rev in opts.revs:
375 idx = self.states.rev2idx(rev)
376 logger.info('try idx=%d rev=%s (command line specified)', idx, rev)
377 yield idx, rev
378 if opts.once:
379 break
380 else:
381 while not self.strategy.is_done():
382 idx = self.strategy.next_idx()
383 rev = self.states.idx2rev(idx)
384 logger.info('try idx=%d rev=%s', idx, rev)
385 yield idx, rev
386 if opts.once:
387 break
388
389 def cmd_run(self, opts):
390 """Performs bisection.
391
392 See run command's help message for more detail.
393
394 Raises:
395 core.VerificationFailed: The bisection range cannot be verified. We
396 expect 'old' at the first rev and 'new' at last rev.
397 core.ExecutionFatalError: Fatal error, bisector stop.
398 strategy.WrongAssumption: Eval results contradicted.
399 """
400 assert self.config.get('switch')
401 assert self.config.get('eval')
402
403 self.strategy.rebuild()
404
405 prev_rev = None
406 for idx, rev in self._next_idx_iter(opts):
407 # Bail out if bisection range is unlikely true in order to prevent
408 # wasting time. This is necessary because some configurations (say,
409 # confidence) may be changed before cmd_run() and thus the bisection
410 # range becomes not acceptable.
411 self.strategy.check_verification_range()
412
413 step, status, values = self._switch_and_eval(rev, prev_rev=prev_rev)
414 logger.info('rev=%s => status %s', rev, status)
415
416 self._add_status(rev, status, values=values)
417 # Bail out if bisection range is unlikely true. Don't save.
418 # The last failing results are likely something wrong in bisection setup,
419 # bisector, and/or evaluator. The benefits of not saving the last failing
420 # results, are that users can just resume bisector directly without needs
421 # of erasing bad results after they fixed the problems.
422 self.strategy.check_verification_range()
423
424 if status == 'skip':
425 current_state = self.states.get(idx)
426 # Bail out if 'skip' too much times. Don't save.
427 if current_state['skip'] > (
428 current_state['old'] + current_state['new'] + 1) * 5:
429 raise core.ExecutionFatalError('too much "skip" for rev=%r' % rev)
430
431 self.states.save()
432
433 self.strategy.show_summary()
434
435 if step == 'switch' and status == 'skip':
436 # Previous switch failed and thus the current version is unknown. Set
437 # it None, so next switch operation won't be bypassed (due to
438 # optimization).
439 prev_rev = None
440 else:
441 prev_rev = rev
442
443 logger.info('done')
444
445 def cmd_view(self, opts):
446 """Shows current progress and candidates."""
447 self.strategy.rebuild()
448 # Rebuild twice in order to re-estimate noise.
449 self.strategy.rebuild()
450 self.strategy.show_summary(more=opts.more)
451 left, right = self.strategy.get_range()
452 self.domain.view(self.states.idx2rev(left), self.states.idx2rev(right))
453
454 def current_status(self, session=None, session_base=None):
455 """Gets current bisect status.
456
457 Returns:
458 A dict describing current status. It contains following items:
459 inited: True iff the session file is initialized (init command has been
460 invoked). If not, below items are omitted.
461 old: Start of current estimated range.
462 new: End of current estimated range.
463 estimated_noise: New estimated noise.
464 done: True if bisection is done, otherwise False.
465 """
466 self._create_states(session=session, session_base=session_base)
467 if self.states.load():
468 self.strategy = strategy.NoisyBinarySearch(
469 self.states.rev_info,
470 self.states.rev2idx(self.config['old']),
471 self.states.rev2idx(self.config['new']),
472 confidence=self.config['confidence'],
473 observation=self.config['noisy'])
474 self.strategy.rebuild()
475 left, right = self.strategy.get_range()
476 estimated_noise = self.strategy.get_noise_observation()
477
478 result = dict(
479 inited=True,
480 old=self.states.idx2rev(left),
481 new=self.states.idx2rev(right),
482 estimated_noise=','.join(estimated_noise),
483 done=self.strategy.is_done())
484 else:
485 result = dict(inited=False)
486 return result
487
488 def cmd_log(self, _opts):
489 """Prints what has been done so far."""
490 for entry in self.states.data['history']:
491 print('{datetime}, {rev} {status_time} {values} {comment_display}'.format(
492 datetime=datetime.datetime.fromtimestamp(entry['time']),
493 status_time=entry['status'] + ('*%d' % entry['times']
494 if entry['times'] > 1 else ''),
495 comment_display=entry['comment'] or '',
496 **entry))
497
498 def cmd_next(self, _opts):
499 """Prints next suggested rev to bisect."""
500 self.strategy.rebuild()
501 if self.strategy.is_done():
502 print('done')
503 return
504
505 idx = self.strategy.next_idx()
506 rev = self.states.idx2rev(idx)
507 print(rev)
508
509 def cmd_switch(self, opts):
510 """Switches to given rev without eval."""
511 assert self.config.get('switch')
512
513 self.strategy.rebuild()
514
515 if opts.rev == 'next':
516 idx = self.strategy.next_idx()
517 rev = self.states.idx2rev(idx)
518 else:
519 rev = self.domain_cls.revtype(opts.rev)
520 assert rev
521
522 logger.info('switch to %s', rev)
523 status = do_switch(self.config['switch'], self.domain, rev)
524 if status:
525 print('switch failed')
526
527 def _add_revs_status_helper(self, revs, status):
528 self.strategy.rebuild()
529 for rev, times in revs:
530 self._add_status(rev, status, times=times, comment='manual')
531 self.states.save()
532
533 def cmd_new(self, opts):
534 """Tells bisect engine the said revs have "new" behavior."""
535 logger.info('set [%s] as new', opts.revs)
536 self._add_revs_status_helper(opts.revs, 'new')
537
538 def cmd_old(self, opts):
539 """Tells bisect engine the said revs have "old" behavior."""
540 logger.info('set [%s] as old', opts.revs)
541 self._add_revs_status_helper(opts.revs, 'old')
542
543 def cmd_skip(self, opts):
544 """Tells bisect engine the said revs have "skip" behavior."""
545 logger.info('set [%s] as skip', opts.revs)
546 self._add_revs_status_helper(opts.revs, 'skip')
547
548 def _create_states(self, session=None, session_base=None):
549 if not session:
550 session = DEFAULT_SESSION_NAME
551 if not session_base:
552 defaults = util.DefaultConfig()
553 session_base = defaults.get('SESSION_BASE', DEFAULT_SESSION_BASE)
554
555 session_file = os.path.join(session_base, session, self.domain_cls.__name__)
556
557 if self.states:
558 assert self.states.session_file == session_file
559 else:
560 self.states = core.States(session_file)
561
562 def cmd_config(self, opts):
563 """Configures additional setting.
564
565 See config command's help message for more detail.
566 """
567 self.states.load()
568 self.domain = self.domain_cls(self.states.config)
569 if not opts.value:
570 print(self.states.config[opts.key])
571 return
572
573 if opts.key in ['switch', 'eval']:
574 self.states.config[opts.key] = opts.value
575
576 elif opts.key == 'confidence':
577 if len(opts.value) != 1:
578 raise core.ExecutionFatalError(
579 '%s config %s: expected 1 value, %d values given' %
580 (sys.argv[0], opts.key, len(opts.value)))
581 try:
582 self.states.config[opts.key] = float(opts.value[0])
583 except ValueError:
584 raise core.ExecutionFatalError('%s config %s: invalid float value: %r' %
585 (sys.argv[0], opts.key, opts.value[0]))
586
587 elif opts.key == 'noisy':
588 if len(opts.value) != 1:
589 raise core.ExecutionFatalError(
590 '%s config %s: expected 1 value, %d values given' %
591 (sys.argv[0], opts.key, len(opts.value)))
592 self.states.config[opts.key] = opts.value[0]
593
594 else:
595 raise core.ExecutionFatalError('%s config: unknown key: %r' %
596 (sys.argv[0], opts.key))
597
598 self.states.save()
599
600 def create_argument_parser(self, prog):
601 defaults = util.DefaultConfig()
602 parser = argparse.ArgumentParser(
603 prog=prog,
604 formatter_class=argparse.RawDescriptionHelpFormatter,
605 description=textwrap.dedent('''\
606 Bisector for %s.
607
608 When running switcher and evaluator, it will set BISECT_REV environment
609 variable, indicates current rev to switch/evaluate.
610 ''' % self.domain_cls.__name__) + textwrap.dedent(self.domain_cls.help))
611 common.add_logging_arguments(parser, defaults)
612 parser.add_argument(
613 '--session_base',
614 type=argtype_dir_path,
615 default=defaults.get('SESSION_BASE', DEFAULT_SESSION_BASE),
616 help='Directory to store sessions (default: %(default)r)')
617 parser.add_argument(
618 '--session',
619 default=DEFAULT_SESSION_NAME,
620 help='Session name (default: %(default)r)')
621 subparsers = parser.add_subparsers(
622 dest='command', title='commands', metavar='<command>')
623
624 parser_reset = subparsers.add_parser(
625 'reset', help='Reset bisect session and clean up saved result')
626 parser_reset.set_defaults(func=self.cmd_reset)
627
628 parser_init = subparsers.add_parser(
629 'init',
630 help='Initializes bisect session',
631 formatter_class=argparse.RawDescriptionHelpFormatter,
632 description=textwrap.dedent('''
633 Besides arguments for 'init' command, you also need to set 'switch'
634 and 'eval' command line via 'config' command.
635 $ bisector config switch <switch command and arguments>
636 $ bisector config eval <eval command and arguments>
637
638 The value of --noisy and --confidence could be changed by 'config'
639 command after 'init' as well.
640 '''))
641 parser_init.add_argument(
642 '--old',
643 required=True,
644 type=self.domain_cls.revtype,
645 help='Start of bisect range, which has old behavior')
646 parser_init.add_argument(
647 '--new',
648 required=True,
649 type=self.domain_cls.revtype,
650 help='End of bisect range, which has new behavior')
651 parser_init.add_argument(
652 '--noisy',
653 help='Enable noisy binary search and specify prior result. '
654 'For example, "old=1/10,new=2/3" means old fail rate is 1/10 '
655 'and new fail rate increased to 2/3. '
656 'Skip if not flaky, say, "new=2/3" means old is always good.')
657 parser_init.add_argument(
658 '--confidence',
659 type=float,
660 default=DEFAULT_CONFIDENCE,
661 help='Confidence level (default: %(default)r)')
662 parser_init.set_defaults(func=self.cmd_init)
663 self.domain_cls.add_init_arguments(parser_init)
664
665 parser_config = subparsers.add_parser(
666 'config', help='Configures additional setting')
667 parser_config.add_argument(
668 'key',
669 choices=['switch', 'eval', 'confidence', 'noisy'],
670 metavar='key',
671 help='What config to change. choices=[%(choices)s]')
672 parser_config.add_argument(
673 'value', nargs=argparse.REMAINDER, help='New value')
674 parser_config.set_defaults(func=self.cmd_config)
675
676 parser_run = subparsers.add_parser(
677 'run',
678 help='Performs bisection',
679 formatter_class=argparse.RawDescriptionHelpFormatter,
680 description=textwrap.dedent('''
681 This command does switch and eval to determine candidates having old or
682 new behavior.
683
684 By default, it attempts to try versions in binary search manner until
685 found the first version having new behavior.
686
687 If version numbers are specified on command line, it just tries those
688 versions and record the result.
689
690 Example:
691 Bisect automatically.
692 $ %(prog)s
693
694 Switch and run version "2.13" and "2.14" and then stop.
695 $ %(prog)s 2.13 2.14
696 '''))
697 parser_run.add_argument(
698 '-1', '--once', action='store_true', help='Only run one step')
699 parser_run.add_argument(
700 'revs',
701 nargs='*',
702 type=self.domain_cls.revtype,
703 help='revs to switch+eval; '
704 'default is calculating automatically and run until done')
705 parser_run.set_defaults(func=self.cmd_run)
706
707 parser_switch = subparsers.add_parser(
708 'switch', help='Switch to given rev without eval')
709 parser_switch.add_argument(
710 'rev', type=argtype_multiplexer(self.domain_cls.revtype, 'next'))
711 parser_switch.set_defaults(func=self.cmd_switch)
712
713 parser_old = subparsers.add_parser(
714 'old', help='Tells bisect engine the said revs have "old" behavior')
715 parser_old.add_argument(
716 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
717 parser_old.set_defaults(func=self.cmd_old)
718
719 parser_new = subparsers.add_parser(
720 'new', help='Tells bisect engine the said revs have "new" behavior')
721 parser_new.add_argument(
722 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
723 parser_new.set_defaults(func=self.cmd_new)
724
725 parser_skip = subparsers.add_parser(
726 'skip', help='Tells bisect engine the said revs have "skip" behavior')
727 parser_skip.add_argument(
728 'revs', nargs='+', type=argtype_multiplier(self.domain_cls.revtype))
729 parser_skip.set_defaults(func=self.cmd_skip)
730
731 parser_view = subparsers.add_parser(
732 'view', help='Shows current progress and candidates')
733 parser_view.add_argument('--more', action='store_true')
734 parser_view.set_defaults(func=self.cmd_view)
735
736 parser_log = subparsers.add_parser(
737 'log', help='Prints what has been done so far')
738 parser_log.set_defaults(func=self.cmd_log)
739
740 parser_next = subparsers.add_parser(
741 'next', help='Prints next suggested rev to bisect')
742 parser_next.set_defaults(func=self.cmd_next)
743
744 return parser
745
746 def main(self, *args, **kwargs):
747 """Command line main function.
748
749 Args:
750 *args: Command line arguments.
751 **kwargs: additional non command line arguments passed by script code.
752 {
753 'prog': Program name; optional.
754 }
755 """
756 parser = self.create_argument_parser(kwargs.get('prog'))
757 opts = parser.parse_args(args or None)
758 common.config_logging(opts)
759
760 self._create_states(session=opts.session, session_base=opts.session_base)
761 if opts.command not in ('init', 'reset', 'config'):
762 self.states.load()
763 self.domain = self.domain_cls(self.states.config)
764 self.strategy = strategy.NoisyBinarySearch(
765 self.states.rev_info,
766 self.states.rev2idx(self.config['old']),
767 self.states.rev2idx(self.config['new']),
768 confidence=self.config['confidence'],
769 observation=self.config['noisy'])
770
771 return opts.func(opts)