blob: 6d27d04dc643ce0721318c0d1d32a8a0f14f011d [file] [log] [blame]
lmrc6bd3a62009-06-10 19:38:06 +00001#!/usr/bin/python
lmr6f669ce2009-05-31 19:02:42 +00002import re, os, sys, StringIO
lmrc6bd3a62009-06-10 19:38:06 +00003import common
lmr6f669ce2009-05-31 19:02:42 +00004from autotest_lib.client.common_lib import error
5
6"""
7KVM configuration file utility functions.
8
9@copyright: Red Hat 2008-2009
10"""
11
12class config:
13 """
14 Parse an input file or string that follows the KVM Test Config File format
15 and generate a list of dicts that will be later used as configuration
16 parameters by the the KVM tests.
17
18 @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File
19 """
20
21 def __init__(self, filename=None, debug=False):
22 """
23 Initialize the list and optionally parse filename.
24
25 @param filename: Path of the file that will be taken
26 """
27 self.list = [{"name": "", "shortname": "", "depend": []}]
28 self.debug = debug
29 self.filename = filename
30 if filename:
31 self.parse_file(filename)
32
33
34 def set_debug(self, debug=False):
35 """
36 Enable or disable debugging output.
37
38 @param debug: Whether debug is enabled (True) or disabled (False).
39 """
40 self.debug = debug
41
42
43 def parse_file(self, filename):
44 """
45 Parse filename, return the resulting list and store it in .list. If
46 filename does not exist, raise an exception.
47
48 @param filename: Path of the configuration file.
49 """
50 if not os.path.exists(filename):
51 raise Exception, "File %s not found" % filename
52 self.filename = filename
53 file = open(filename, "r")
54 self.list = self.parse(file, self.list)
55 file.close()
56 return self.list
57
58
59 def parse_string(self, str):
60 """
61 Parse a string, return the resulting list and store it in .list.
62
63 @param str: String that will be parsed.
64 """
65 file = StringIO.StringIO(str)
66 self.list = self.parse(file, self.list)
67 file.close()
68 return self.list
69
70
71 def get_list(self):
72 """
73 Return the list of dictionaries. This should probably be called after
74 parsing something.
75 """
76 return self.list
77
78
79 def match(self, filter, dict):
80 """
81 Return True if dict matches filter.
82
83 @param filter: A regular expression that defines the filter.
84 @param dict: Dictionary that will be inspected.
85 """
86 filter = re.compile("(\\.|^)" + filter + "(\\.|$)")
87 return filter.search(dict["name"]) != None
88
89
90 def filter(self, filter, list=None):
91 """
92 Filter a list of dicts.
93
94 @param filter: A regular expression that will be used as a filter.
95 @param list: A list of dictionaries that will be filtered.
96 """
97 if list == None:
98 list = self.list
99 filtered_list = []
100 for dict in list:
101 if self.match(filter, dict):
102 filtered_list.append(dict)
103 return filtered_list
104
105
106 # Currently unused, will be removed if it remains unused
107 def get_match_block_indices(self, filter, list=None):
108 """
109 Get the indexes of a list that match a given filter.
110
111 @param filter: A regular expression that will filter the list.
112 @param list: List which we want to know the indexes that match
113 a filter.
114 """
115 if list == None:
116 list = self.list
117 block_list = []
118 prev_match = False
119 for index in range(len(list)):
120 dict = list[index]
121 if self.match(filter, dict):
122 if not prev_match:
123 block_list.append([index])
124 prev_match = True
125 else:
126 if prev_match:
127 block_list[-1].append(index)
128 prev_match = False
129 if prev_match:
130 block_list[-1].append(len(list))
131 return block_list
132
133
134 def split_and_strip(self, str, sep="="):
135 """
136 Split str and strip quotes from the resulting parts.
137
138 @param str: String that will be processed
139 @param sep: Separator that will be used to split the string
140 """
lmr0015c712009-06-08 13:51:14 +0000141 temp = str.split(sep, 1)
lmr6f669ce2009-05-31 19:02:42 +0000142 for i in range(len(temp)):
143 temp[i] = temp[i].strip()
lmr434252b2009-06-10 19:27:14 +0000144 if re.findall("^\".*\"$", temp[i]):
145 temp[i] = temp[i].strip("\"")
146 elif re.findall("^\'.*\'$", temp[i]):
147 temp[i] = temp[i].strip("\'")
lmr6f669ce2009-05-31 19:02:42 +0000148 return temp
149
150
151 def get_next_line(self, file):
152 """
153 Get the next non-empty, non-comment line in a file like object.
154
155 @param file: File like object
156 @return: If no line is available, return None.
157 """
158 while True:
159 line = file.readline()
160 if line == "": return None
161 stripped_line = line.strip()
162 if len(stripped_line) > 0 \
163 and not stripped_line.startswith('#') \
164 and not stripped_line.startswith('//'):
165 return line
166
167
168 def get_next_line_indent(self, file):
169 """
170 Return the indent level of the next non-empty, non-comment line in file.
171
172 @param file: File like object.
173 @return: If no line is available, return -1.
174 """
175 pos = file.tell()
176 line = self.get_next_line(file)
177 if not line:
178 file.seek(pos)
179 return -1
180 line = line.expandtabs()
181 indent = 0
182 while line[indent] == ' ':
183 indent += 1
184 file.seek(pos)
185 return indent
186
187
188 def add_name(self, str, name, append=False):
189 """
190 Add name to str with a separator dot and return the result.
191
192 @param str: String that will be processed
193 @param name: name that will be appended to the string.
194 @return: If append is True, append name to str.
195 Otherwise, pre-pend name to str.
196 """
197 if str == "":
198 return name
199 # Append?
200 elif append:
201 return str + "." + name
202 # Prepend?
203 else:
204 return name + "." + str
205
206
207 def parse_variants(self, file, list, subvariants=False, prev_indent=-1):
208 """
209 Read and parse lines from file like object until a line with an indent
210 level lower than or equal to prev_indent is encountered.
211
212 @brief: Parse a 'variants' or 'subvariants' block from a file-like
213 object.
214 @param file: File-like object that will be parsed
215 @param list: List of dicts to operate on
216 @param subvariants: If True, parse in 'subvariants' mode;
217 otherwise parse in 'variants' mode
218 @param prev_indent: The indent level of the "parent" block
219 @return: The resulting list of dicts.
220 """
221 new_list = []
222
223 while True:
224 indent = self.get_next_line_indent(file)
225 if indent <= prev_indent:
226 break
227 indented_line = self.get_next_line(file).rstrip()
228 line = indented_line.strip()
229
230 # Get name and dependencies
231 temp = line.strip("- ").split(":")
232 name = temp[0]
233 if len(temp) == 1:
234 dep_list = []
235 else:
236 dep_list = temp[1].split()
237
238 # See if name should be added to the 'shortname' field
239 add_to_shortname = True
240 if name.startswith("@"):
241 name = name.strip("@")
242 add_to_shortname = False
243
244 # Make a deep copy of list
245 temp_list = []
246 for dict in list:
247 new_dict = dict.copy()
248 new_dict["depend"] = dict["depend"][:]
249 temp_list.append(new_dict)
250
251 if subvariants:
252 # If we're parsing 'subvariants', we need to modify the list first
253 self.__modify_list_subvariants(temp_list, name, dep_list, add_to_shortname)
254 temp_list = self.parse(file, temp_list,
255 restricted=True, prev_indent=indent)
256 else:
257 # If we're parsing 'variants', we need to parse first and then modify the list
258 if self.debug:
259 self.__debug_print(indented_line, "Entering variant '%s' (variant inherits %d dicts)" % (name, len(list)))
260 temp_list = self.parse(file, temp_list,
261 restricted=False, prev_indent=indent)
262 self.__modify_list_variants(temp_list, name, dep_list, add_to_shortname)
263
264 new_list += temp_list
265
266 return new_list
267
268
269 def parse(self, file, list, restricted=False, prev_indent=-1):
270 """
271 Read and parse lines from file until a line with an indent level lower
272 than or equal to prev_indent is encountered.
273
274 @brief: Parse a file-like object.
275 @param file: A file-like object
276 @param list: A list of dicts to operate on (list is modified in
277 place and should not be used after the call)
278 @param restricted: if True, operate in restricted mode
279 (prohibit 'variants')
280 @param prev_indent: the indent level of the "parent" block
281 @return: Return the resulting list of dicts.
282 @note: List is destroyed and should not be used after the call.
283 Only the returned list should be used.
284 """
285 while True:
286 indent = self.get_next_line_indent(file)
287 if indent <= prev_indent:
288 break
289 indented_line = self.get_next_line(file).rstrip()
290 line = indented_line.strip()
291 words = line.split()
292
293 len_list = len(list)
294
295 # Look for a known operator in the line
296 operators = ["?+=", "?<=", "?=", "+=", "<=", "="]
297 op_found = None
298 for op in operators:
299 if op in line:
300 op_found = op
301 break
302
303 # Found an operator?
304 if op_found:
305 if self.debug and not restricted:
306 self.__debug_print(indented_line,
307 "Parsing operator (%d dicts in current"
308 "context)" % len_list)
309 (left, value) = self.split_and_strip(line, op_found)
310 filters_and_key = self.split_and_strip(left, ":")
311 filters = filters_and_key[:-1]
312 key = filters_and_key[-1]
313 filtered_list = list
314 for filter in filters:
315 filtered_list = self.filter(filter, filtered_list)
316 # Apply the operation to the filtered list
317 for dict in filtered_list:
318 if op_found == "=":
319 dict[key] = value
320 elif op_found == "+=":
321 dict[key] = dict.get(key, "") + value
322 elif op_found == "<=":
323 dict[key] = value + dict.get(key, "")
324 elif op_found.startswith("?") and dict.has_key(key):
325 if op_found == "?=":
326 dict[key] = value
327 elif op_found == "?+=":
328 dict[key] = dict.get(key, "") + value
329 elif op_found == "?<=":
330 dict[key] = value + dict.get(key, "")
331
332 # Parse 'no' and 'only' statements
333 elif words[0] == "no" or words[0] == "only":
334 if len(words) <= 1:
335 continue
336 filters = words[1:]
337 filtered_list = []
338 if words[0] == "no":
339 for dict in list:
340 for filter in filters:
341 if self.match(filter, dict):
342 break
343 else:
344 filtered_list.append(dict)
345 if words[0] == "only":
346 for dict in list:
347 for filter in filters:
348 if self.match(filter, dict):
349 filtered_list.append(dict)
350 break
351 list = filtered_list
352 if self.debug and not restricted:
353 self.__debug_print(indented_line,
354 "Parsing no/only (%d dicts in current"
355 "context, %d remain)" %
356 (len_list, len(list)))
357
358 # Parse 'variants'
359 elif line == "variants:":
360 # 'variants' not allowed in restricted mode
361 # (inside an exception or inside subvariants)
362 if restricted:
363 e_msg = "Using variants in this context is not allowed"
lmrc6bd3a62009-06-10 19:38:06 +0000364 raise error.AutotestError(e_msg)
lmr6f669ce2009-05-31 19:02:42 +0000365 if self.debug and not restricted:
366 self.__debug_print(indented_line,
367 "Entering variants block (%d dicts in"
368 "current context)" % len_list)
369 list = self.parse_variants(file, list, subvariants=False,
370 prev_indent=indent)
371
372 # Parse 'subvariants' (the block is parsed for each dict
373 # separately)
374 elif line == "subvariants:":
375 if self.debug and not restricted:
376 self.__debug_print(indented_line,
377 "Entering subvariants block (%d dicts in"
378 "current context)" % len_list)
379 new_list = []
380 # Remember current file position
381 pos = file.tell()
382 # Read the lines in any case
383 self.parse_variants(file, [], subvariants=True,
384 prev_indent=indent)
385 # Iterate over the list...
386 for index in range(len(list)):
387 # Revert to initial file position in this 'subvariants'
388 # block
389 file.seek(pos)
390 # Everything inside 'subvariants' should be parsed in
391 # restricted mode
392 new_list += self.parse_variants(file, list[index:index+1],
393 subvariants=True,
394 prev_indent=indent)
395 list = new_list
396
397 # Parse 'include' statements
398 elif words[0] == "include":
399 if len(words) <= 1:
400 continue
401 if self.debug and not restricted:
402 self.__debug_print(indented_line,
403 "Entering file %s" % words[1])
404 if self.filename:
405 filename = os.path.join(os.path.dirname(self.filename),
406 words[1])
407 if not os.path.exists(filename):
408 e_msg = "Cannot include %s -- file not found" % filename
409 raise error.AutotestError(e_msg)
410 new_file = open(filename, "r")
411 list = self.parse(new_file, list, restricted)
412 new_file.close()
413 if self.debug and not restricted:
414 self.__debug_print("", "Leaving file %s" % words[1])
415 else:
416 e_msg = "Cannot include anything because no file is open"
417 raise error.AutotestError(e_msg)
418
419 # Parse multi-line exceptions
420 # (the block is parsed for each dict separately)
421 elif line.endswith(":"):
422 if self.debug and not restricted:
423 self.__debug_print(indented_line,
424 "Entering multi-line exception block"
425 "(%d dicts in current context outside"
426 "exception)" % len_list)
427 line = line.strip(":")
428 new_list = []
429 # Remember current file position
430 pos = file.tell()
431 # Read the lines in any case
432 self.parse(file, [], restricted=True, prev_indent=indent)
433 # Iterate over the list...
434 for index in range(len(list)):
435 if self.match(line, list[index]):
436 # Revert to initial file position in this
437 # exception block
438 file.seek(pos)
439 # Everything inside an exception should be parsed in
440 # restricted mode
441 new_list += self.parse(file, list[index:index+1],
442 restricted=True, prev_indent=indent)
443 else:
444 new_list += list[index:index+1]
445 list = new_list
446
447 return list
448
449
450 def __debug_print(self, str1, str2=""):
451 """
452 Nicely print two strings and an arrow.
453
454 @param str1: First string
455 @param str2: Second string
456 """
457 if str2:
458 str = "%-50s ---> %s" % (str1, str2)
459 else:
460 str = str1
461 print str
462
463
464 def __modify_list_variants(self, list, name, dep_list, add_to_shortname):
465 """
466 Make some modifications to list, as part of parsing a 'variants' block.
467
468 @param list
469 """
470 for dict in list:
471 # Prepend name to the dict's 'name' field
472 dict["name"] = self.add_name(dict["name"], name)
473 # Prepend name to the dict's 'shortname' field
474 if add_to_shortname:
475 dict["shortname"] = self.add_name(dict["shortname"], name)
476 # Prepend name to each of the dict's dependencies
477 for i in range(len(dict["depend"])):
478 dict["depend"][i] = self.add_name(dict["depend"][i], name)
479 # Add new dependencies
480 dict["depend"] += dep_list
481
482
483 def __modify_list_subvariants(self, list, name, dep_list, add_to_shortname):
484 """
485 Make some modifications to list, as part of parsing a
486 'subvariants' block.
487
488 @param list: List that will be processed
489 @param name: Name that will be prepended to the dictionary name
490 @param dep_list: List of dependencies to be added to the list
491 dictionaries
492 @param add_to_shortname: Whether we'll add a shortname parameter to
493 the dictionaries.
494 """
495 for dict in list:
496 # Add new dependencies
497 for dep in dep_list:
498 dep_name = self.add_name(dict["name"], dep, append=True)
499 dict["depend"].append(dep_name)
500 # Append name to the dict's 'name' field
501 dict["name"] = self.add_name(dict["name"], name, append=True)
502 # Append name to the dict's 'shortname' field
503 if add_to_shortname:
504 dict["shortname"] = self.add_name(dict["shortname"], name,
505 append=True)
506
507
508if __name__ == "__main__":
509 if len(sys.argv) >= 2:
510 filename = sys.argv[1]
511 else:
512 filename = os.path.join(os.path.dirname(sys.argv[0]), "kvm_tests.cfg")
513 list = config(filename, debug=True).get_list()
514 i = 0
515 for dict in list:
516 print "Dictionary #%d:" % i
517 keys = dict.keys()
518 keys.sort()
519 for key in keys:
520 print " %s = %s" % (key, dict[key])
521 i += 1