blob: 900bbb7d476ca2a0ec85a4adcaff9d89d4a7f16b [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.
Kuang-che Wu2526a672019-09-10 16:23:59 +08005"""Command line utility functions."""
Kuang-che Wu88875db2017-07-20 10:47:53 +08006
7from __future__ import print_function
8import argparse
Kuang-che Wu88875db2017-07-20 10:47:53 +08009import logging
10import os
11import re
Kuang-che Wu443633f2019-02-27 00:58:33 +080012import signal
Kuang-che Wufe1e88a2019-09-10 21:52:25 +080013import sys
Kuang-che Wu88875db2017-07-20 10:47:53 +080014
Kuang-che Wufe1e88a2019-09-10 21:52:25 +080015from bisect_kit import errors
Kuang-che Wu88875db2017-07-20 10:47:53 +080016from bisect_kit import util
17
18logger = logging.getLogger(__name__)
19
Kuang-che Wu0476d1f2019-03-04 19:27:01 +080020# Exit code of bisect eval script. These values are chosen compatible with 'git
21# bisect'.
22EXIT_CODE_OLD = 0
23EXIT_CODE_NEW = 1
24EXIT_CODE_SKIP = 125
25EXIT_CODE_FATAL = 128
26
Kuang-che Wu88875db2017-07-20 10:47:53 +080027
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))
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +080043 super().__init__(full_msg)
Kuang-che Wu88875db2017-07-20 10:47:53 +080044
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))
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +080072 except ValueError as e:
73 raise ArgTypeError('should be a number', '123') from e
Kuang-che Wu88875db2017-07-20 10:47:53 +080074
75
Kuang-che Wu603cdad2019-01-18 21:32:55 +080076def argtype_re(pattern, example):
77 r"""Validate argument matches `pattern`.
78
79 Args:
80 pattern: regex pattern
81 example: example string which matches `pattern`
82
83 Returns:
84 A new argtype function which matches regex `pattern`
85 """
86 assert re.match(pattern, example)
87
88 def validate(s):
89 if re.match(pattern, s):
90 return s
91 if re.escape(pattern) == pattern:
92 raise ArgTypeError('should be "%s"' % pattern, pattern)
93 raise ArgTypeError('should match "%s"' % pattern,
94 '"%s" like %s' % (pattern, example))
95
96 return validate
97
98
Kuang-che Wu88875db2017-07-20 10:47:53 +080099def argtype_multiplexer(*args):
100 r"""argtype multiplexer
101
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800102 This function takes a list of argtypes and creates a new function matching
103 them. Moreover, it gives error message with examples.
Kuang-che Wu88875db2017-07-20 10:47:53 +0800104
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800105 Examples:
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800106 >>> argtype = argtype_multiplexer(argtype_int,
107 argtype_re(r'^r\d+$', 'r123'))
Kuang-che Wu88875db2017-07-20 10:47:53 +0800108 >>> argtype('123')
109 123
110 >>> argtype('r456')
111 r456
112 >>> argtype('hello')
Kuang-che Wu603cdad2019-01-18 21:32:55 +0800113 ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123)
Kuang-che Wu88875db2017-07-20 10:47:53 +0800114
115 Args:
116 *args: list of argtypes or regex pattern.
117
118 Returns:
119 A new argtype function which matches *args.
120 """
121
122 def validate(s):
123 examples = []
124 for t in args:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800125 try:
126 return t(s)
127 except ArgTypeError as e:
128 examples += e.example
129
130 msg = 'Invalid argument'
131 raise ArgTypeError(msg, examples)
132
133 return validate
134
135
136def argtype_multiplier(argtype):
137 """A new argtype that supports multiplier suffix of the given argtype.
138
Kuang-che Wubaaa4532018-08-15 17:08:10 +0800139 Examples:
Kuang-che Wu88875db2017-07-20 10:47:53 +0800140 Supports the given argtype accepting "foo" as argument, this function
141 generates a new argtype function which accepts argument like "foo*3".
142
143 Returns:
144 A new argtype function which returns (arg, times) where arg is accepted
145 by input `argtype` and times is repeating count. Note that if multiplier is
146 omitted, "times" is 1.
147 """
148
149 def helper(s):
150 m = re.match(r'^(.+)\*(\d+)$', s)
151 try:
152 if m:
153 return argtype(m.group(1)), int(m.group(2))
Kuang-che Wu68db08a2018-03-30 11:50:34 +0800154 return argtype(s), 1
Kuang-che Wu88875db2017-07-20 10:47:53 +0800155 except ArgTypeError as e:
156 # It should be okay to gives multiplier example only for the first one
157 # because it is just "example", no need to enumerate all possibilities.
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800158 raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3']) from e
Kuang-che Wu88875db2017-07-20 10:47:53 +0800159
160 return helper
161
162
163def argtype_dir_path(s):
164 """Validate argument is an existing directory.
165
166 Args:
167 s: string to validate.
168
169 Raises:
170 ArgTypeError if the path is not a directory.
171 """
172 if not os.path.exists(s):
173 raise ArgTypeError('should be an existing directory', '/path/to/somewhere')
174 if not os.path.isdir(s):
175 raise ArgTypeError('should be a directory', '/path/to/somewhere')
176
177 # Normalize, trim trailing path separators.
178 if len(s) > 1 and s[-1] == os.path.sep:
179 s = s[:-1]
180 return s
181
182
Kuang-che Wu88518882017-09-22 16:57:25 +0800183def check_executable(program):
184 """Checks whether a program is executable.
185
186 Args:
187 program: program path in question
188
189 Returns:
190 string as error message if `program` is not executable, or None otherwise.
191 It will return None if unable to determine as well.
192 """
193 returncode = util.call('which', program)
194 if returncode == 127: # No 'which' on this platform, skip the check.
195 return None
196 if returncode == 0: # is executable
197 return None
198
199 hint = ''
200 if not os.path.exists(program):
201 hint = 'Not in PATH?'
202 elif not os.path.isfile(program):
203 hint = 'Not a file'
204 elif not os.access(program, os.X_OK):
205 hint = 'Forgot to chmod +x?'
206 elif '/' not in program:
207 hint = 'Forgot to prepend "./" ?'
208 return '%r is not executable. %s' % (program, hint)
209
210
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800211def lookup_signal_name(signum):
212 """Look up signal name by signal number.
213
214 Args:
215 signum: signal number
216
217 Returns:
218 signal name, like "SIGTERM". "Unknown" for unexpected number.
219 """
220 for k, v in vars(signal).items():
221 if k.startswith('SIG') and '_' not in k and v == signum:
222 return k
223 return 'Unknown'
224
225
Kuang-che Wu443633f2019-02-27 00:58:33 +0800226def format_returncode(returncode):
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800227 # returncode is negative if the process is terminated by signal directly.
Kuang-che Wu443633f2019-02-27 00:58:33 +0800228 if returncode < 0:
229 signum = -returncode
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800230 signame = lookup_signal_name(signum)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800231 return 'terminated by signal %d (%s)' % (signum, signame)
Kuang-che Wubc7bfce2019-05-21 18:43:16 +0800232 # Some programs, e.g. shell, handled signal X and exited with (128 + X).
233 if returncode > 128:
234 signum = returncode - 128
235 signame = lookup_signal_name(signum)
236 return 'exited with code %d; may be signal %d (%s)' % (returncode, signum,
237 signame)
Kuang-che Wu443633f2019-02-27 00:58:33 +0800238
239 return 'exited with code %d' % returncode
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800240
241
242def patching_argparser_exit(parser):
243 """Patching argparse.ArgumentParser.exit to exit with fatal exit code.
244
245 Args:
246 parser: argparse.ArgumentParser object
247 """
248 orig_exit = parser.exit
249
250 def exit_hack(status=0, message=None):
251 if status != 0:
252 status = EXIT_CODE_FATAL
253 orig_exit(status, message)
254
255 parser.exit = exit_hack
256
257
258def fatal_error_handler(func):
259 """Function decorator which exits with fatal code for fatal exceptions.
260
261 This is a helper for switcher and evaluator. It catches fatal exceptions and
262 exits with fatal exit code. The fatal exit code (128) is aligned with 'git
263 bisect'.
264
265 See also argparse_fatal_error().
266
267 Args:
268 func: wrapped function
269 """
270
271 def wrapper(*args, **kwargs):
272 try:
273 return func(*args, **kwargs)
274 except (AssertionError, errors.ExecutionFatalError, errors.ArgumentError):
Kuang-che Wu6157f1a2019-09-11 21:29:42 +0800275 logger.exception('fatal exception, bisection should stop')
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800276 sys.exit(EXIT_CODE_FATAL)
277
278 return wrapper