1# Copyright 2018 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
5"""Common config and logic for binary search tool
6
7This module serves two main purposes:
8  1. Programatically include the utils module in PYTHONPATH
9  2. Create the argument parsing shared between binary_search_state.py and
10     bisect.py
11
12The argument parsing is handled by populating _ArgsDict with all arguments.
13_ArgsDict is required so that binary_search_state.py and bisect.py can share
14the argument parsing, but treat them slightly differently. For example,
15bisect.py requires that all argument defaults are suppressed so that overriding
16can occur properly (i.e. only options that are explicitly entered by the user
17end up in the resultant options dictionary).
18
19ArgumentDict inherits OrderedDict in order to preserve the order the args are
20created so the help text is made properly.
21"""
22
23from __future__ import print_function
24
25import collections
26import os
27import sys
28
29# Programatically adding utils python path to PYTHONPATH
30if os.path.isabs(sys.argv[0]):
31  utils_pythonpath = os.path.abspath('{0}/..'.format(
32      os.path.dirname(sys.argv[0])))
33else:
34  wdir = os.getcwd()
35  utils_pythonpath = os.path.abspath('{0}/{1}/..'.format(wdir, os.path.dirname(
36      sys.argv[0])))
37sys.path.append(utils_pythonpath)
38
39
40class ArgumentDict(collections.OrderedDict):
41  """Wrapper around OrderedDict, represents CLI arguments for program.
42
43  AddArgument enforces the following layout:
44  {
45      ['-n', '--iterations'] : {
46          'dest': 'iterations',
47          'type': int,
48          'help': 'Number of iterations to try in the search.',
49          'default': 50
50      }
51      [arg_name1, arg_name2, ...] : {
52          arg_option1 : arg_option_val1,
53          ...
54      },
55      ...
56  }
57  """
58  _POSSIBLE_OPTIONS = ['action', 'nargs', 'const', 'default', 'type', 'choices',
59                       'required', 'help', 'metavar', 'dest']
60
61  def AddArgument(self, *args, **kwargs):
62    """Add argument to ArgsDict, has same signature as argparse.add_argument
63
64    Emulates the the argparse.add_argument method so the internal OrderedDict
65    can be safely and easily populated. Each call to this method will have a 1-1
66    corresponding call to argparse.add_argument once BuildArgParser is called.
67
68    Args:
69      *args: The names for the argument (-V, --verbose, etc.)
70      **kwargs: The options for the argument, corresponds to the args of
71                argparse.add_argument
72
73    Returns:
74      None
75
76    Raises:
77      TypeError: if args is empty or if option in kwargs is not a valid
78                 option for argparse.add_argument.
79    """
80    if len(args) == 0:
81      raise TypeError('Argument needs at least one name')
82
83    for key in kwargs:
84      if key not in self._POSSIBLE_OPTIONS:
85        raise TypeError('Invalid option "%s" for argument %s' % (key, args[0]))
86
87    self[args] = kwargs
88
89
90_ArgsDict = ArgumentDict()
91
92
93def GetArgsDict():
94  """_ArgsDict singleton method"""
95  if not _ArgsDict:
96    _BuildArgsDict(_ArgsDict)
97  return _ArgsDict
98
99
100def BuildArgParser(parser, override=False):
101  """Add all arguments from singleton ArgsDict to parser.
102
103  Will take argparse parser and add all arguments in ArgsDict. Will ignore
104  the default and required options if override is set to True.
105
106  Args:
107    parser: type argparse.ArgumentParser, will call add_argument for every item
108            in _ArgsDict
109    override: True if being called from bisect.py. Used to say that default and
110              required options are to be ignored
111
112  Returns:
113    None
114  """
115  ArgsDict = GetArgsDict()
116
117  # Have no defaults when overriding
118  for arg_names, arg_options in ArgsDict.iteritems():
119    if override:
120      arg_options = arg_options.copy()
121      arg_options.pop('default', None)
122      arg_options.pop('required', None)
123
124    parser.add_argument(*arg_names, **arg_options)
125
126
127def StrToBool(str_in):
128  if str_in.lower() in ['true', 't', '1']:
129    return True
130  if str_in.lower() in ['false', 'f', '0']:
131    return False
132
133  raise AttributeError('%s is not a valid boolean string' % str_in)
134
135
136def _BuildArgsDict(args):
137  """Populate ArgumentDict with all arguments"""
138  args.AddArgument(
139      '-n',
140      '--iterations',
141      dest='iterations',
142      type=int,
143      help='Number of iterations to try in the search.',
144      default=50)
145  args.AddArgument(
146      '-i',
147      '--get_initial_items',
148      dest='get_initial_items',
149      help='Script to run to get the initial objects. '
150           'If your script requires user input '
151           'the --verbose option must be used')
152  args.AddArgument(
153      '-g',
154      '--switch_to_good',
155      dest='switch_to_good',
156      help='Script to run to switch to good. '
157           'If your switch script requires user input '
158           'the --verbose option must be used')
159  args.AddArgument(
160      '-b',
161      '--switch_to_bad',
162      dest='switch_to_bad',
163      help='Script to run to switch to bad. '
164           'If your switch script requires user input '
165           'the --verbose option must be used')
166  args.AddArgument(
167      '-I',
168      '--test_setup_script',
169      dest='test_setup_script',
170      help='Optional script to perform building, flashing, '
171           'and other setup before the test script runs.')
172  args.AddArgument(
173      '-t',
174      '--test_script',
175      dest='test_script',
176      help='Script to run to test the '
177           'output after packages are built.')
178  # No input (evals to False),
179  # --prune (evals to True),
180  # --prune=False,
181  # --prune=True
182  args.AddArgument(
183      '-p',
184      '--prune',
185      dest='prune',
186      nargs='?',
187      const=True,
188      default=False,
189      type=StrToBool,
190      metavar='bool',
191      help='If True, continue until all bad items are found. '
192            'Defaults to False.')
193  args.AddArgument(
194      '-P',
195      '--pass_bisect',
196      dest='pass_bisect',
197      default=None,
198      help='Script to generate another script for pass level bisect, '
199           'which contains command line options to build bad item. '
200           'This will also turn on pass/transformation level bisection. '
201           'Needs support of `-opt-bisect-limit`(pass) and '
202           '`-print-debug-counter`(transformation) from LLVM. '
203           'For now it only supports one single bad item, so to use it, '
204           'prune must be set to False.')
205  # No input (evals to False),
206  # --noincremental (evals to True),
207  # --noincremental=False,
208  # --noincremental=True
209  args.AddArgument(
210      '-c',
211      '--noincremental',
212      dest='noincremental',
213      nargs='?',
214      const=True,
215      default=False,
216      type=StrToBool,
217      metavar='bool',
218      help='If True, don\'t propagate good/bad changes '
219           'incrementally. Defaults to False.')
220  # No input (evals to False),
221  # --file_args (evals to True),
222  # --file_args=False,
223  # --file_args=True
224  args.AddArgument(
225      '-f',
226      '--file_args',
227      dest='file_args',
228      nargs='?',
229      const=True,
230      default=False,
231      type=StrToBool,
232      metavar='bool',
233      help='Whether to use a file to pass arguments to scripts. '
234           'Defaults to False.')
235  # No input (evals to True),
236  # --verify (evals to True),
237  # --verify=False,
238  # --verify=True
239  args.AddArgument(
240      '--verify',
241      dest='verify',
242      nargs='?',
243      const=True,
244      default=True,
245      type=StrToBool,
246      metavar='bool',
247      help='Whether to run verify iterations before searching. '
248           'Defaults to True.')
249  args.AddArgument(
250      '-N',
251      '--prune_iterations',
252      dest='prune_iterations',
253      type=int,
254      help='Number of prune iterations to try in the search.',
255      default=100)
256  # No input (evals to False),
257  # --verbose (evals to True),
258  # --verbose=False,
259  # --verbose=True
260  args.AddArgument(
261      '-V',
262      '--verbose',
263      dest='verbose',
264      nargs='?',
265      const=True,
266      default=False,
267      type=StrToBool,
268      metavar='bool',
269      help='If True, print full output to console.')
270  args.AddArgument(
271      '-r',
272      '--resume',
273      dest='resume',
274      action='store_true',
275      help='Resume bisection tool execution from state file.'
276           'Useful if the last bisection was terminated '
277           'before it could properly finish.')
278