virt.virt_test_utils: run_autotest - 'tar' needs relative paths to strip the leading '/'
[autotest-zwu.git] / client / common_lib / cartesian_config.py
blobac04c24c3cec8f742a04835c2a3b69ffa951603f
1 #!/usr/bin/python
2 """
3 Cartesian configuration format file parser.
5 Filter syntax:
6 , means OR
7 .. means AND
8 . means IMMEDIATELY-FOLLOWED-BY
10 Example:
11 qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide
12 means match all dicts whose names have:
13 (qcow2 AND (Fedora IMMEDIATELY-FOLLOWED-BY 14)) OR
14 ((RHEL IMMEDIATELY-FOLLOWED-BY 6) AND raw AND boot) OR
15 (smp2 AND qcow2 AND migrate AND ide)
17 Note:
18 'qcow2..Fedora.14' is equivalent to 'Fedora.14..qcow2'.
19 'qcow2..Fedora.14' is not equivalent to 'qcow2..14.Fedora'.
20 'ide, scsi' is equivalent to 'scsi, ide'.
22 Filters can be used in 3 ways:
23 only <filter>
24 no <filter>
25 <filter>:
26 The last one starts a conditional block.
28 @copyright: Red Hat 2008-2011
29 """
31 import re, os, sys, optparse, collections
33 class ParserError:
34 def __init__(self, msg, line=None, filename=None, linenum=None):
35 self.msg = msg
36 self.line = line
37 self.filename = filename
38 self.linenum = linenum
40 def __str__(self):
41 if self.line:
42 return "%s: %r (%s:%s)" % (self.msg, self.line,
43 self.filename, self.linenum)
44 else:
45 return "%s (%s:%s)" % (self.msg, self.filename, self.linenum)
48 num_failed_cases = 5
51 class Node(object):
52 def __init__(self):
53 self.name = []
54 self.dep = []
55 self.content = []
56 self.children = []
57 self.labels = set()
58 self.append_to_shortname = False
59 self.failed_cases = collections.deque()
62 def _match_adjacent(block, ctx, ctx_set):
63 # TODO: explain what this function does
64 if block[0] not in ctx_set:
65 return 0
66 if len(block) == 1:
67 return 1
68 if block[1] not in ctx_set:
69 return int(ctx[-1] == block[0])
70 k = 0
71 i = ctx.index(block[0])
72 while i < len(ctx):
73 if k > 0 and ctx[i] != block[k]:
74 i -= k - 1
75 k = 0
76 if ctx[i] == block[k]:
77 k += 1
78 if k >= len(block):
79 break
80 if block[k] not in ctx_set:
81 break
82 i += 1
83 return k
86 def _might_match_adjacent(block, ctx, ctx_set, descendant_labels):
87 matched = _match_adjacent(block, ctx, ctx_set)
88 for elem in block[matched:]:
89 if elem not in descendant_labels:
90 return False
91 return True
94 # Filter must inherit from object (otherwise type() won't work)
95 class Filter(object):
96 def __init__(self, s):
97 self.filter = []
98 for char in s:
99 if not (char.isalnum() or char.isspace() or char in ".,_-"):
100 raise ParserError("Illegal characters in filter")
101 for word in s.replace(",", " ").split():
102 word = [block.split(".") for block in word.split("..")]
103 for block in word:
104 for elem in block:
105 if not elem:
106 raise ParserError("Syntax error")
107 self.filter += [word]
110 def match(self, ctx, ctx_set):
111 for word in self.filter:
112 for block in word:
113 if _match_adjacent(block, ctx, ctx_set) != len(block):
114 break
115 else:
116 return True
117 return False
120 def might_match(self, ctx, ctx_set, descendant_labels):
121 for word in self.filter:
122 for block in word:
123 if not _might_match_adjacent(block, ctx, ctx_set,
124 descendant_labels):
125 break
126 else:
127 return True
128 return False
131 class NoOnlyFilter(Filter):
132 def __init__(self, line):
133 Filter.__init__(self, line.split(None, 1)[1])
134 self.line = line
137 class OnlyFilter(NoOnlyFilter):
138 def is_irrelevant(self, ctx, ctx_set, descendant_labels):
139 return self.match(ctx, ctx_set)
142 def requires_action(self, ctx, ctx_set, descendant_labels):
143 return not self.might_match(ctx, ctx_set, descendant_labels)
146 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
147 descendant_labels):
148 for word in self.filter:
149 for block in word:
150 if (_match_adjacent(block, ctx, ctx_set) >
151 _match_adjacent(block, failed_ctx, failed_ctx_set)):
152 return self.might_match(ctx, ctx_set, descendant_labels)
153 return False
156 class NoFilter(NoOnlyFilter):
157 def is_irrelevant(self, ctx, ctx_set, descendant_labels):
158 return not self.might_match(ctx, ctx_set, descendant_labels)
161 def requires_action(self, ctx, ctx_set, descendant_labels):
162 return self.match(ctx, ctx_set)
165 def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
166 descendant_labels):
167 for word in self.filter:
168 for block in word:
169 if (_match_adjacent(block, ctx, ctx_set) <
170 _match_adjacent(block, failed_ctx, failed_ctx_set)):
171 return not self.match(ctx, ctx_set)
172 return False
175 class Condition(NoFilter):
176 def __init__(self, line):
177 Filter.__init__(self, line.rstrip(":"))
178 self.line = line
179 self.content = []
182 class NegativeCondition(OnlyFilter):
183 def __init__(self, line):
184 Filter.__init__(self, line.lstrip("!").rstrip(":"))
185 self.line = line
186 self.content = []
189 class Parser(object):
191 Parse an input file or string that follows the Cartesian Config File format
192 and generate a list of dicts that will be later used as configuration
193 parameters by autotest tests that use that format.
195 @see: http://autotest.kernel.org/wiki/CartesianConfig
198 def __init__(self, filename=None, debug=False):
200 Initialize the parser and optionally parse a file.
202 @param filename: Path of the file to parse.
203 @param debug: Whether to turn on debugging output.
205 self.node = Node()
206 self.debug = debug
207 if filename:
208 self.parse_file(filename)
211 def parse_file(self, filename):
213 Parse a file.
215 @param filename: Path of the configuration file.
217 self.node = self._parse(FileReader(filename), self.node)
220 def parse_string(self, s):
222 Parse a string.
224 @param s: String to parse.
226 self.node = self._parse(StrReader(s), self.node)
229 def get_dicts(self, node=None, ctx=[], content=[], shortname=[], dep=[]):
231 Generate dictionaries from the code parsed so far. This should
232 be called after parsing something.
234 @return: A dict generator.
236 def process_content(content, failed_filters):
237 # 1. Check that the filters in content are OK with the current
238 # context (ctx).
239 # 2. Move the parts of content that are still relevant into
240 # new_content and unpack conditional blocks if appropriate.
241 # For example, if an 'only' statement fully matches ctx, it
242 # becomes irrelevant and is not appended to new_content.
243 # If a conditional block fully matches, its contents are
244 # unpacked into new_content.
245 # 3. Move failed filters into failed_filters, so that next time we
246 # reach this node or one of its ancestors, we'll check those
247 # filters first.
248 for t in content:
249 filename, linenum, obj = t
250 if type(obj) is Op:
251 new_content.append(t)
252 continue
253 # obj is an OnlyFilter/NoFilter/Condition/NegativeCondition
254 if obj.requires_action(ctx, ctx_set, labels):
255 # This filter requires action now
256 if type(obj) is OnlyFilter or type(obj) is NoFilter:
257 self._debug(" filter did not pass: %r (%s:%s)",
258 obj.line, filename, linenum)
259 failed_filters.append(t)
260 return False
261 else:
262 self._debug(" conditional block matches: %r (%s:%s)",
263 obj.line, filename, linenum)
264 # Check and unpack the content inside this Condition
265 # object (note: the failed filters should go into
266 # new_internal_filters because we don't expect them to
267 # come from outside this node, even if the Condition
268 # itself was external)
269 if not process_content(obj.content,
270 new_internal_filters):
271 failed_filters.append(t)
272 return False
273 continue
274 elif obj.is_irrelevant(ctx, ctx_set, labels):
275 # This filter is no longer relevant and can be removed
276 continue
277 else:
278 # Keep the filter and check it again later
279 new_content.append(t)
280 return True
282 def might_pass(failed_ctx,
283 failed_ctx_set,
284 failed_external_filters,
285 failed_internal_filters):
286 for t in failed_external_filters:
287 if t not in content:
288 return True
289 filename, linenum, filter = t
290 if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
291 labels):
292 return True
293 for t in failed_internal_filters:
294 filename, linenum, filter = t
295 if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
296 labels):
297 return True
298 return False
300 def add_failed_case():
301 node.failed_cases.appendleft((ctx, ctx_set,
302 new_external_filters,
303 new_internal_filters))
304 if len(node.failed_cases) > num_failed_cases:
305 node.failed_cases.pop()
307 node = node or self.node
308 # Update dep
309 for d in node.dep:
310 dep = dep + [".".join(ctx + [d])]
311 # Update ctx
312 ctx = ctx + node.name
313 ctx_set = set(ctx)
314 labels = node.labels
315 # Get the current name
316 name = ".".join(ctx)
317 if node.name:
318 self._debug("checking out %r", name)
319 # Check previously failed filters
320 for i, failed_case in enumerate(node.failed_cases):
321 if not might_pass(*failed_case):
322 self._debug(" this subtree has failed before")
323 del node.failed_cases[i]
324 node.failed_cases.appendleft(failed_case)
325 return
326 # Check content and unpack it into new_content
327 new_content = []
328 new_external_filters = []
329 new_internal_filters = []
330 if (not process_content(node.content, new_internal_filters) or
331 not process_content(content, new_external_filters)):
332 add_failed_case()
333 return
334 # Update shortname
335 if node.append_to_shortname:
336 shortname = shortname + node.name
337 # Recurse into children
338 count = 0
339 for n in node.children:
340 for d in self.get_dicts(n, ctx, new_content, shortname, dep):
341 count += 1
342 yield d
343 # Reached leaf?
344 if not node.children:
345 self._debug(" reached leaf, returning it")
346 d = {"name": name, "dep": dep, "shortname": ".".join(shortname)}
347 for filename, linenum, op in new_content:
348 op.apply_to_dict(d)
349 yield d
350 # If this node did not produce any dicts, remember the failed filters
351 # of its descendants
352 elif not count:
353 new_external_filters = []
354 new_internal_filters = []
355 for n in node.children:
356 (failed_ctx,
357 failed_ctx_set,
358 failed_external_filters,
359 failed_internal_filters) = n.failed_cases[0]
360 for obj in failed_internal_filters:
361 if obj not in new_internal_filters:
362 new_internal_filters.append(obj)
363 for obj in failed_external_filters:
364 if obj in content:
365 if obj not in new_external_filters:
366 new_external_filters.append(obj)
367 else:
368 if obj not in new_internal_filters:
369 new_internal_filters.append(obj)
370 add_failed_case()
373 def _debug(self, s, *args):
374 if self.debug:
375 s = "DEBUG: %s" % s
376 print s % args
379 def _warn(self, s, *args):
380 s = "WARNING: %s" % s
381 print s % args
384 def _parse_variants(self, cr, node, prev_indent=-1):
386 Read and parse lines from a FileReader object until a line with an
387 indent level lower than or equal to prev_indent is encountered.
389 @param cr: A FileReader/StrReader object.
390 @param node: A node to operate on.
391 @param prev_indent: The indent level of the "parent" block.
392 @return: A node object.
394 node4 = Node()
396 while True:
397 line, indent, linenum = cr.get_next_line(prev_indent)
398 if not line:
399 break
401 name, dep = map(str.strip, line.lstrip("- ").split(":", 1))
402 for char in name:
403 if not (char.isalnum() or char in "@._-"):
404 raise ParserError("Illegal characters in variant name",
405 line, cr.filename, linenum)
406 for char in dep:
407 if not (char.isalnum() or char.isspace() or char in ".,_-"):
408 raise ParserError("Illegal characters in dependencies",
409 line, cr.filename, linenum)
411 node2 = Node()
412 node2.children = [node]
413 node2.labels = node.labels
415 node3 = self._parse(cr, node2, prev_indent=indent)
416 node3.name = name.lstrip("@").split(".")
417 node3.dep = dep.replace(",", " ").split()
418 node3.append_to_shortname = not name.startswith("@")
420 node4.children += [node3]
421 node4.labels.update(node3.labels)
422 node4.labels.update(node3.name)
424 return node4
427 def _parse(self, cr, node, prev_indent=-1):
429 Read and parse lines from a StrReader object until a line with an
430 indent level lower than or equal to prev_indent is encountered.
432 @param cr: A FileReader/StrReader object.
433 @param node: A Node or a Condition object to operate on.
434 @param prev_indent: The indent level of the "parent" block.
435 @return: A node object.
437 while True:
438 line, indent, linenum = cr.get_next_line(prev_indent)
439 if not line:
440 break
442 words = line.split(None, 1)
444 # Parse 'variants'
445 if line == "variants:":
446 # 'variants' is not allowed inside a conditional block
447 if (isinstance(node, Condition) or
448 isinstance(node, NegativeCondition)):
449 raise ParserError("'variants' is not allowed inside a "
450 "conditional block",
451 None, cr.filename, linenum)
452 node = self._parse_variants(cr, node, prev_indent=indent)
453 continue
455 # Parse 'include' statements
456 if words[0] == "include":
457 if len(words) < 2:
458 raise ParserError("Syntax error: missing parameter",
459 line, cr.filename, linenum)
460 filename = os.path.expanduser(words[1])
461 if isinstance(cr, FileReader) and not os.path.isabs(filename):
462 filename = os.path.join(os.path.dirname(cr.filename),
463 filename)
464 if not os.path.isfile(filename):
465 self._warn("%r (%s:%s): file doesn't exist or is not a "
466 "regular file", line, cr.filename, linenum)
467 continue
468 node = self._parse(FileReader(filename), node)
469 continue
471 # Parse 'only' and 'no' filters
472 if words[0] in ("only", "no"):
473 if len(words) < 2:
474 raise ParserError("Syntax error: missing parameter",
475 line, cr.filename, linenum)
476 try:
477 if words[0] == "only":
478 f = OnlyFilter(line)
479 elif words[0] == "no":
480 f = NoFilter(line)
481 except ParserError, e:
482 e.line = line
483 e.filename = cr.filename
484 e.linenum = linenum
485 raise
486 node.content += [(cr.filename, linenum, f)]
487 continue
489 # Look for operators
490 op_match = _ops_exp.search(line)
492 # Parse conditional blocks
493 if ":" in line:
494 index = line.index(":")
495 if not op_match or index < op_match.start():
496 index += 1
497 cr.set_next_line(line[index:], indent, linenum)
498 line = line[:index]
499 try:
500 if line.startswith("!"):
501 cond = NegativeCondition(line)
502 else:
503 cond = Condition(line)
504 except ParserError, e:
505 e.line = line
506 e.filename = cr.filename
507 e.linenum = linenum
508 raise
509 self._parse(cr, cond, prev_indent=indent)
510 node.content += [(cr.filename, linenum, cond)]
511 continue
513 # Parse regular operators
514 if not op_match:
515 raise ParserError("Syntax error", line, cr.filename, linenum)
516 node.content += [(cr.filename, linenum, Op(line, op_match))]
518 return node
521 # Assignment operators
523 _reserved_keys = set(("name", "shortname", "dep"))
526 def _op_set(d, key, value):
527 if key not in _reserved_keys:
528 d[key] = value
531 def _op_append(d, key, value):
532 if key not in _reserved_keys:
533 d[key] = d.get(key, "") + value
536 def _op_prepend(d, key, value):
537 if key not in _reserved_keys:
538 d[key] = value + d.get(key, "")
541 def _op_regex_set(d, exp, value):
542 exp = re.compile("%s$" % exp)
543 for key in d:
544 if key not in _reserved_keys and exp.match(key):
545 d[key] = value
548 def _op_regex_append(d, exp, value):
549 exp = re.compile("%s$" % exp)
550 for key in d:
551 if key not in _reserved_keys and exp.match(key):
552 d[key] += value
555 def _op_regex_prepend(d, exp, value):
556 exp = re.compile("%s$" % exp)
557 for key in d:
558 if key not in _reserved_keys and exp.match(key):
559 d[key] = value + d[key]
562 def _op_regex_del(d, empty, exp):
563 exp = re.compile("%s$" % exp)
564 for key in d.keys():
565 if key not in _reserved_keys and exp.match(key):
566 del d[key]
569 _ops = {"=": (r"\=", _op_set),
570 "+=": (r"\+\=", _op_append),
571 "<=": (r"\<\=", _op_prepend),
572 "?=": (r"\?\=", _op_regex_set),
573 "?+=": (r"\?\+\=", _op_regex_append),
574 "?<=": (r"\?\<\=", _op_regex_prepend),
575 "del": (r"^del\b", _op_regex_del)}
577 _ops_exp = re.compile("|".join([op[0] for op in _ops.values()]))
580 class Op(object):
581 def __init__(self, line, m):
582 self.func = _ops[m.group()][1]
583 self.key = line[:m.start()].strip()
584 value = line[m.end():].strip()
585 if value and (value[0] == value[-1] == '"' or
586 value[0] == value[-1] == "'"):
587 value = value[1:-1]
588 self.value = value
591 def apply_to_dict(self, d):
592 self.func(d, self.key, self.value)
595 # StrReader and FileReader
597 class StrReader(object):
599 Preprocess an input string for easy reading.
601 def __init__(self, s):
603 Initialize the reader.
605 @param s: The string to parse.
607 self.filename = "<string>"
608 self._lines = []
609 self._line_index = 0
610 self._stored_line = None
611 for linenum, line in enumerate(s.splitlines()):
612 line = line.rstrip().expandtabs()
613 stripped_line = line.lstrip()
614 indent = len(line) - len(stripped_line)
615 if (not stripped_line
616 or stripped_line.startswith("#")
617 or stripped_line.startswith("//")):
618 continue
619 self._lines.append((stripped_line, indent, linenum + 1))
622 def get_next_line(self, prev_indent):
624 Get the next line in the current block.
626 @param prev_indent: The indentation level of the previous block.
627 @return: (line, indent, linenum), where indent is the line's
628 indentation level. If no line is available, (None, -1, -1) is
629 returned.
631 if self._stored_line:
632 ret = self._stored_line
633 self._stored_line = None
634 return ret
635 if self._line_index >= len(self._lines):
636 return None, -1, -1
637 line, indent, linenum = self._lines[self._line_index]
638 if indent <= prev_indent:
639 return None, -1, -1
640 self._line_index += 1
641 return line, indent, linenum
644 def set_next_line(self, line, indent, linenum):
646 Make the next call to get_next_line() return the given line instead of
647 the real next line.
649 line = line.strip()
650 if line:
651 self._stored_line = line, indent, linenum
654 class FileReader(StrReader):
656 Preprocess an input file for easy reading.
658 def __init__(self, filename):
660 Initialize the reader.
662 @parse filename: The name of the input file.
664 StrReader.__init__(self, open(filename).read())
665 self.filename = filename
668 if __name__ == "__main__":
669 parser = optparse.OptionParser('usage: %prog [options] filename '
670 '[extra code] ...\n\nExample:\n\n '
671 '%prog tests.cfg "only my_set" "no qcow2"')
672 parser.add_option("-v", "--verbose", dest="debug", action="store_true",
673 help="include debug messages in console output")
674 parser.add_option("-f", "--fullname", dest="fullname", action="store_true",
675 help="show full dict names instead of short names")
676 parser.add_option("-c", "--contents", dest="contents", action="store_true",
677 help="show dict contents")
679 options, args = parser.parse_args()
680 if not args:
681 parser.error("filename required")
683 c = Parser(args[0], debug=options.debug)
684 for s in args[1:]:
685 c.parse_string(s)
687 for i, d in enumerate(c.get_dicts()):
688 if options.fullname:
689 print "dict %4d: %s" % (i + 1, d["name"])
690 else:
691 print "dict %4d: %s" % (i + 1, d["shortname"])
692 if options.contents:
693 keys = d.keys()
694 keys.sort()
695 for key in keys:
696 print " %s = %s" % (key, d[key])