Whamcloud - gitweb
LU-12461 contrib: Add epython scripts for crash dump analysis
[fs/lustre-release.git] / contrib / debug_tools / epython_scripts / crashlib / input / __init__.py
1
2 """
3 Input handling routines
4 Copyright 2014 Cray Inc.  All Rights Reserved
5 """
6
7
8 import itertools
9
10
11 # Define some common integer multiplier suffixes
12
13 # Powers of two
14 binary_suffixes={
15     'k': 2**10, 'K': 2**10,
16     'm': 2**20, 'M': 2**20,
17     'g': 2**30, 'G': 2**30,
18     't': 2**40, 'T': 2**40,
19     'p': 2**50, 'P': 2**50
20 }
21 memory_suffixes = binary_suffixes
22
23 # Powers of ten
24 decimal_suffixes={
25     'k': 10**3,  'K': 10**3,
26     'm': 10**6,  'M': 10**6,
27     'g': 10**9,  'G': 10**9,
28     't': 10**12, 'T': 10**12,
29     'p': 10**15, 'P': 10**15
30 }
31 disk_suffixes = decimal_suffixes
32
33 default_bases = [0, 16]
34
35 def toint(string, base=default_bases, suffixes=binary_suffixes):
36     """Convert to integer with flexible base and multiplier support.
37
38     Provide a way to handle input that may be in any of several number
39     bases but may not use the appropriate prefix, e.g. 'deadbeef' rather
40     than the more pedantic '0xdeadbeef'. Also provide support for
41     multiplier suffixes, such as 'K' for kilo.
42
43     Arguments:
44
45         string      - string to convert to integer
46         base        - a single number, as used in int() or an iterable
47                       of base values to try
48         suffixes    - dictionary keyed by the string suffix with a value
49                       to be used as a multiplier
50
51     The default base of [0, 16] allows the automatic recognition of numbers
52     with the standard prefixes and if that fails, tries a base 16 conversion.
53     """
54     try:
55         bases = list(base)
56     except TypeError:
57         # Object isn't iterable, so create one that is
58         bases = [base]
59
60     for b in bases:
61         if not (b == 0 or 2 <= b <= 36):
62             raise ValueError(
63                 "toint() base {!s:s} must be >= 2 and <= 36".format(b))
64
65     multiplier = 1
66     try:
67         # Second iteration is after removing any suffix.  This way, if
68         # a suffix happens to contain valid numeric characters, we'll
69         # try the numeric interpretation before we try their multiplier
70         # meaning, e.g. 'g' is a valid numeric value in base 17).
71         for i in xrange(2):
72             for b in bases:
73                 try:
74                     return int(string, b) * multiplier
75                 except ValueError:
76                     pass
77
78             if i != 0:
79                 raise ValueError
80
81             # Find a suffix that matches the end of the string and use it
82             for k, v in suffixes.iteritems():
83                 if string.endswith(k):
84                     multiplier = v
85                     string = string[0:-len(k)]
86                     break
87             else:
88                 raise ValueError
89
90     except ValueError:
91         suffix_list = suffixes.keys()
92         suffix_list.sort()
93         raise ValueError(
94             "invalid literal '{:s}' for toint() with base {!s:s} "
95             "and suffixes {!s:s}".format(string, list(bases), suffix_list))
96
97
98 def hex2int(string):
99     """Wrapper for toint() which prefers base 16 input
100
101     This function is useful in situations where a callable must be passed,
102     such as with argparse.add_argument(type=hex2int, ...
103     """
104     return toint(string, base=[16, 0])
105
106
107 def to_rangelist(args, default=xrange(0), base=[0,16],
108                   suffixes=binary_suffixes):
109     """Convert a bunch of range list strings into a list of ranges
110
111     The arguments are:
112
113         args     - iterable containing ranglist strings
114         default  - iterator to return if args is empty
115         base     - number base to use for integer conversion
116         suffixes - integer multiplier suffixes
117
118     Each arg is taken to be a range list, where a range list may be:
119
120         rangelist ::= range[,range]...
121         range     ::= <first>-<last> | <first>#<count> | <value>
122
123     where the range first-last is inclusive.
124     """
125     if len(args) == 0:
126         return default
127
128     ranges = []
129     for range_list_str in args:
130         range_strs = range_list_str.split(',')
131         for range_str in range_strs:
132             if "-" in range_str:
133                 fields = range_str.split('-', 1)
134                 start = toint(fields[0], base, suffixes=suffixes)
135                 end = toint(fields[1], base, suffixes=suffixes) + 1
136                 ranges.append(xrange(start, end))
137             elif "#" in range_str:
138                 fields = range_str.split('#', 1)
139                 start = toint(fields[0], base, suffixes=suffixes)
140                 end = start + toint(fields[1], base, suffixes=suffixes)
141                 ranges.append(xrange(start, end))
142             else:
143                 start = toint(range_str, base, suffixes=suffixes)
144                 end = start + 1
145                 ranges.append(xrange(start, end))
146
147     return ranges
148
149
150 def iter_rangestr(*args, **kwargs):
151     """Convert a bunch of range list strings into a single iterator
152
153     The arguments are the same as for to_rangelist().
154     """
155     return itertools.chain(*to_rangelist(*args, **kwargs))
156
157
158 if __name__ == '__main__':
159     import unittest
160
161     # toint()
162     class Test_toint(unittest.TestCase):
163         def test_base_zero(self):
164             self.assertEqual(toint('0b10', 0), 2)
165             self.assertEqual(toint('0o10', 0), 8)
166             self.assertEqual(toint('10', 0), 10)
167             self.assertEqual(toint('0x10', 0), 16)
168
169         def test_base_out_of_range(self):
170             self.assertRaises(ValueError, toint, '10', -1)
171             self.assertRaises(ValueError, toint, '10',  1)
172             self.assertRaises(ValueError, toint, '10', 37)
173
174         def test_base_search(self):
175             bases = [0, 16]
176             self.assertEqual(toint('10', bases), 10)
177             self.assertEqual(toint('f', bases), 15)
178
179             self.assertEqual(toint('0b10', bases), 2)
180             self.assertEqual(toint('0o10', bases), 8)
181             self.assertEqual(toint('10', bases), 10)
182             self.assertEqual(toint('0x10', bases), 16)
183
184         def test_suffixes(self):
185             for k, v in binary_suffixes.iteritems():
186                 self.assertEqual(toint('0b10'+k), 0b10*v)
187                 self.assertEqual(toint('0o10'+k), 0o10*v)
188                 self.assertEqual(toint('10'+k), 10*v)
189                 self.assertEqual(toint('0x10'+k), 0x10*v)
190
191         def test_suffix_number_overlap(self):
192             # Verify a valid numeric isn't used as a suffix
193             self.assertEqual(toint('1g', 17), 33)
194             self.assertEqual(toint('1gk', 17), 33*binary_suffixes['k'])
195
196
197     # hex2int()
198     class Test_hex2int(unittest.TestCase):
199         """Verify the hex2int() function"""
200         def test_explicit_base(self):
201             """Verify that explicit base syntax is honored"""
202             self.assertEqual(hex2int('0x10'), 16)
203             self.assertEqual(hex2int('0o10'), 8)
204
205         def test_default_base(self):
206             """Verify that base 16 is preferred"""
207             self.assertEqual(hex2int('10'), 16)
208             self.assertEqual(hex2int('0b10'), 2832)
209
210
211     # iter_rangelist()
212     class Test_iter_rangelist(unittest.TestCase):
213         """Test both iter_rangelist and the underlying to_rangelist."""
214         def test_good_single_ranges(self):
215             self.assertEqual(list(iter_rangestr([])), [])
216             self.assertEqual(list(iter_rangestr(['1-2'])), list(xrange(1,3)))
217             self.assertEqual(list(iter_rangestr(['1#2'])), list(xrange(1,3)))
218             self.assertEqual(list(iter_rangestr(['1'])), list(xrange(1,2)))
219
220         def test_good_multiple_ranges(self):
221             test_rangestrs = [
222                 # Test params,        Expected result
223                 (['1', '3-5', '1#2'], [1, 3, 4, 5, 1, 2]),
224                 ]
225
226             for ranges, expected in test_rangestrs:
227                 # Test the ranges as separate list elements
228                 self.assertEqual(list(iter_rangestr(ranges)), expected)
229
230                 # Test the ranges joined by commas
231                 joined = [','.join(ranges)]
232                 self.assertEqual(list(iter_rangestr(joined)), expected)
233
234         def test_bad_single_ranges(self):
235             self.assertRaises(ValueError, iter_rangestr, ['1#2#3'])
236             self.assertRaises(ValueError, iter_rangestr, ['1#2-3'])
237             self.assertRaises(ValueError, iter_rangestr, ['1-2#3'])
238             self.assertRaises(ValueError, iter_rangestr, ['1-2-3'])
239
240     # Run all unit tests
241     unittest.main()