Re: [KVM-AUTOTEST PATCH] KVM test: refactor kvm_config.py

Ryan Harper
2011-02-09T16:06:10Z

* Michael Goldish <mgoldish@redhat.com> [2011-02-08 19:51]:
> This is a reimplementation of the dict generator. It is much faster than the
> current implementation and uses a very small amount of memory. Running time
> and memory usage scale polynomially with the number of defined variants,
> compared to exponentially in the current implementation.

Thanks for looking at this. I know I've run into all of this when
running a complicated configuration on a lower memory system.

>
> Instead of regular expressions in the filters, the following syntax is used:
>
> , means OR
> .. means AND
> . means IMMEDIATELY-FOLLOWED-BY

Is there any reason we can't use | for or, and & for AND? I know this
is just nit picking, but, it certainly reads easier and doesn't need a
translation. AFAICT, in the implementation, we're just using .split(),
so, I think the delimiters aren't critical.

>
> Example:
>
> only qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide
>
> means select all dicts whose names have:
>
> (qcow2 AND (Fedora IMMEDIATELY-FOLLOWED-BY 14)) OR
> ((RHEL IMMEDIATELY-FOLLOWED-BY 6) AND raw AND boot) OR
> (smp2 AND qcow2 AND migrate AND ide)

>>> config = "qcow2&Fedora.14|RHEL.6&raw&boot|smp2&qcow2&migrate&ide"
>>> config
'qcow2&Fedora.14|RHEL.6&raw&boot|smp2&qcow2&migrate&ide'
>>> config.split("|")
['qcow2&Fedora.14', 'RHEL.6&raw&boot', 'smp2&qcow2&migrate&ide']

>
> 'qcow2..Fedora.14' is equivalent to 'Fedora.14..qcow2'.
> 'qcow2..Fedora.14' is not equivalent to 'qcow2..14.Fedora'.
> 'ide, scsi' is equivalent to 'scsi, ide'.
>
> Filters can be used in 3 ways:
> only <filter>
> no <filter>
> <filter>:
>
> The last one starts a conditional block, e.g.
>
> Fedora.14..qcow2:
> no migrate, reboot
> foo = bar
>
> Interface changes:
> - The main class is now called 'Parser' instead of 'config'.
> - fork_and_parse() has been removed. parse_file() and parse_string() should be
> used instead.
> - When run as a standalone program, kvm_config.py just prints the shortnames of
> the generated dicts by default, and can optionally print the full names and
> contents of the dicts.
> - By default, debug messages are not printed, but they can be enabled by
> passing debug=True to Parser's constructor, or by running kvm_config.py -v.
> - The 'depend' key has been renamed to 'dep'.
>
> Signed-off-by: Michael Goldish <mgoldish@redhat.com>
> Signed-off-by: Uri Lublin <ulublin@redhat.com>
> ---
> client/tests/kvm/control | 28 +-
> client/tests/kvm/control.parallel | 12 +-
> client/tests/kvm/kvm_config.py | 1051 ++++++++++++++------------------
> client/tests/kvm/kvm_scheduler.py | 9 +-
> client/tests/kvm/kvm_utils.py | 2 +-
> client/tests/kvm/tests.cfg.sample | 13 +-
> client/tests/kvm/tests_base.cfg.sample | 46 +-
> 7 files changed, 513 insertions(+), 648 deletions(-)
>
> diff --git a/client/tests/kvm/control b/client/tests/kvm/control
> index d226adf..be37678 100644
> --- a/client/tests/kvm/control
> +++ b/client/tests/kvm/control
> @@ -35,13 +35,11 @@ str = """
> # build configuration here. For example:
> #release_tag = 84
> """
> -build_cfg = kvm_config.config()
> -# As the base test config is quite large, in order to save memory, we use the
> -# fork_and_parse() method, that creates another parser process and destroys it
> -# at the end of the parsing, so the memory spent can be given back to the OS.
> -build_cfg_path = os.path.join(kvm_test_dir, "build.cfg")
> -build_cfg.fork_and_parse(build_cfg_path, str)
> -if not kvm_utils.run_tests(build_cfg.get_generator(), job):
> +
> +parser = kvm_config.Parser()
> +parser.parse_file(os.path.join(kvm_test_dir, "build.cfg"))
> +parser.parse_string(str)
> +if not kvm_utils.run_tests(parser.get_dicts(), job):
> logging.error("KVM build step failed, exiting.")
> sys.exit(1)
>
> @@ -49,10 +47,11 @@ str = """
> # This string will be parsed after tests.cfg. Make any desired changes to the
> # test configuration here. For example:
> #display = sdl
> -#install|setup: timeout_multiplier = 3
> +#install, setup: timeout_multiplier = 3
> """
> -tests_cfg = kvm_config.config()
> -tests_cfg_path = os.path.join(kvm_test_dir, "tests.cfg")
> +
> +parser = kvm_config.Parser()
> +parser.parse_file(os.path.join(kvm_test_dir, "tests.cfg"))
>
> if args:
> # We get test parameters from command line
> @@ -67,11 +66,12 @@ if args:
> str += "%s = %s\n" % (key, value)
> except IndexError:
> pass
> -tests_cfg.fork_and_parse(tests_cfg_path, str)
> +parser.parse_string(str)
>
> -# Run the tests
> -kvm_utils.run_tests(tests_cfg.get_generator(), job)
> +logging.info("Selected tests:")
> +for i, d in enumerate(parser.get_dicts()):
> + logging.info("Test %4d: %s" % (i + 1, d["shortname"]))
> +kvm_utils.run_tests(parser.get_dicts(), job)
>
> # Generate a nice HTML report inside the job's results dir
> kvm_utils.create_report(kvm_test_dir, job.resultdir)
> -
> diff --git a/client/tests/kvm/control.parallel b/client/tests/kvm/control.parallel
> index ac84638..640ccf5 100644
> --- a/client/tests/kvm/control.parallel
> +++ b/client/tests/kvm/control.parallel
> @@ -163,16 +163,15 @@ import kvm_config
> str = """
> # This string will be parsed after tests.cfg. Make any desired changes to the
> # test configuration here. For example:
> -#install|setup: timeout_multiplier = 3
> -#only fc8_quick
> +#install, setup: timeout_multiplier = 3
> #display = sdl
> """
> -cfg = kvm_config.config()
> -filename = os.path.join(pwd, "tests.cfg")
> -cfg.fork_and_parse(filename, str)
>
> -tests = cfg.get_list()
> +parser = kvm_config.Parser()
> +parser.parse_file(os.path.join(pwd, "tests.cfg"))
> +parser.parse_string(str)
>
> +tests = list(parser.get_dicts())
>
> # -------------
> # Run the tests
> @@ -192,7 +191,6 @@ s = kvm_scheduler.scheduler(tests, num_workers, total_cpus, total_mem, pwd)
> job.parallel([s.scheduler],
> *[(s.worker, i, job.run_test) for i in range(num_workers)])
>
> -
> # create the html report in result dir
> reporter = os.path.join(pwd, 'make_html_report.py')
> html_file = os.path.join(job.resultdir,'results.html')
> diff --git a/client/tests/kvm/kvm_config.py b/client/tests/kvm/kvm_config.py
> index 13cdfe2..1b27181 100755
> --- a/client/tests/kvm/kvm_config.py
> +++ b/client/tests/kvm/kvm_config.py
> @@ -1,18 +1,149 @@
> #!/usr/bin/python
> """
> -KVM configuration file utility functions.
> +KVM test configuration file parser
>
> -@copyright: Red Hat 2008-2010
> +@copyright: Red Hat 2008-2011
> """
>
> -import logging, re, os, sys, optparse, array, traceback, cPickle
> -import common
> -import kvm_utils
> -from autotest_lib.client.common_lib import error
> -from autotest_lib.client.common_lib import logging_manager
> +import re, os, sys, optparse, collections
> +
> +
> +# Filter syntax:
> +# , means OR
> +# .. means AND
> +# . means IMMEDIATELY-FOLLOWED-BY
> +
> +# Example:
> +# qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide
> +# means match all dicts whose names have:
> +# (qcow2 AND (Fedora IMMEDIATELY-FOLLOWED-BY 14)) OR
> +# ((RHEL IMMEDIATELY-FOLLOWED-BY 6) AND raw AND boot) OR
> +# (smp2 AND qcow2 AND migrate AND ide)
> +
> +# Note:
> +# 'qcow2..Fedora.14' is equivalent to 'Fedora.14..qcow2'.
> +# 'qcow2..Fedora.14' is not equivalent to 'qcow2..14.Fedora'.
> +# 'ide, scsi' is equivalent to 'scsi, ide'.
> +
> +# Filters can be used in 3 ways:
> +# only <filter>
> +# no <filter>
> +# <filter>:
> +# The last one starts a conditional block.
> +
> +
> +num_failed_cases = 5
> +
> +
> +class Node(object):
> + def __init__(self):
> + self.name = []
> + self.dep = []
> + self.content = []
> + self.children = []
> + self.labels = set()
> + self.append_to_shortname = False
> + self.failed_cases = collections.deque()
> +
> +
> +# Filter must inherit from object (otherwise type() won't work)
> +class Filter(object):
> + def __init__(self, s):
> + self.filter = []
> + for word in s.replace(",", " ").split():
> + word = [block.split(".") for block in word.split("..")]
> + self.filter += [word]
> +
> +
> + def match_adjacent(self, block, ctx, ctx_set):
> + # TODO: explain what this function does
> + if block[0] not in ctx_set:
> + return 0
> + if len(block) == 1:
> + return 1
> + if block[1] not in ctx_set:
> + return int(ctx[-1] == block[0])
> + k = 0
> + i = ctx.index(block[0])
> + while i < len(ctx):
> + if k > 0 and ctx[i] != block[k]:
> + i -= k - 1
> + k = 0
> + if ctx[i] == block[k]:
> + k += 1
> + if k >= len(block):
> + break
> + if block[k] not in ctx_set:
> + break
> + i += 1
> + return k
> +
> +
> + def might_match_adjacent(self, block, ctx, ctx_set, descendant_labels):
> + matched = self.match_adjacent(block, ctx, ctx_set)
> + for elem in block[matched:]:
> + if elem not in descendant_labels:
> + return False
> + return True
> +
> +
> + def match(self, ctx, ctx_set):
> + for word in self.filter:
> + for block in word:
> + if self.match_adjacent(block, ctx, ctx_set) != len(block):
> + break
> + else:
> + return True
> + return False
> +
> +
> + def might_match(self, ctx, ctx_set, descendant_labels):
> + for word in self.filter:
> + for block in word:
> + if not self.might_match_adjacent(block, ctx, ctx_set,
> + descendant_labels):
> + break
> + else:
> + return True
> + return False
> +
> +
> +class NoOnlyFilter(Filter):
> + def __init__(self, line):
> + Filter.__init__(self, line.split(None, 1)[1])
> + self.line = line
> +
> +
> +class OnlyFilter(NoOnlyFilter):
> + def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
> + descendant_labels):
> + for word in self.filter:
> + for block in word:
> + if (self.match_adjacent(block, ctx, ctx_set) >
> + self.match_adjacent(block, failed_ctx, failed_ctx_set)):
> + return self.might_match(ctx, ctx_set, descendant_labels)
> + return False
> +
>
> +class NoFilter(NoOnlyFilter):
> + def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
> + descendant_labels):
> + for word in self.filter:
> + for block in word:
> + if (self.match_adjacent(block, ctx, ctx_set) <
> + self.match_adjacent(block, failed_ctx, failed_ctx_set)):
> + return not self.match(ctx, ctx_set)
> + return False
>
> -class config:
> +
> +class Condition(NoFilter):
> + def __init__(self, line):
> + Filter.__init__(self, line.rstrip(":"))
> + self.line = line
> + self.content = []
> +
> +
> +class Parser(object):
> """
> Parse an input file or string that follows the KVM Test Config File format
> and generate a list of dicts that will be later used as configuration
> @@ -21,17 +152,14 @@ class config:
> @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File
> """
>
> - def __init__(self, filename=None, debug=True):
> + def __init__(self, filename=None, debug=False):
> """
> - Initialize the list and optionally parse a file.
> + Initialize the parser and optionally parse a file.
>
> - @param filename: Path of the file that will be taken.
> + @param filename: Path of the file to parse.
> @param debug: Whether to turn on debugging output.
> """
> - self.list = [array.array("H", [4, 4, 4, 4])]
> - self.object_cache = []
> - self.object_cache_indices = {}
> - self.regex_cache = {}
> + self.node = Node()
> self.debug = debug
> if filename:
> self.parse_file(filename)
> @@ -39,689 +167,436 @@ class config:
>
> def parse_file(self, filename):
> """
> - Parse file. If it doesn't exist, raise an IOError.
> + Parse a file.
>
> @param filename: Path of the configuration file.
> """
> - if not os.path.exists(filename):
> - raise IOError("File %s not found" % filename)
> - str = open(filename).read()
> - self.list = self.parse(configreader(filename, str), self.list)
> + self.node = self._parse(FileReader(filename), self.node)
>
>
> - def parse_string(self, str):
> + def parse_string(self, s):
> """
> Parse a string.
>
> - @param str: String to parse.
> + @param s: String to parse.
> """
> - self.list = self.parse(configreader('<string>', str, real_file=False), self.list)
> + self.node = self._parse(StrReader(s), self.node)
>
>
> - def fork_and_parse(self, filename=None, str=None):
> - """
> - Parse a file and/or a string in a separate process to save memory.
> -
> - Python likes to keep memory to itself even after the objects occupying
> - it have been destroyed. If during a call to parse_file() or
> - parse_string() a lot of memory is used, it can only be freed by
> - terminating the process. This function works around the problem by
> - doing the parsing in a forked process and then terminating it, freeing
> - any unneeded memory.
> -
> - Note: if an exception is raised during parsing, its information will be
> - printed, and the resulting list will be empty. The exception will not
> - be raised in the process calling this function.
> -
> - @param filename: Path of file to parse (optional).
> - @param str: String to parse (optional).
> - """
> - r, w = os.pipe()
> - r, w = os.fdopen(r, "r"), os.fdopen(w, "w")
> - pid = os.fork()
> - if not pid:
> - # Child process
> - r.close()
> - try:
> - if filename:
> - self.parse_file(filename)
> - if str:
> - self.parse_string(str)
> - except:
> - traceback.print_exc()
> - self.list = []
> - # Convert the arrays to strings before pickling because at least
> - # some Python versions can't pickle/unpickle arrays
> - l = [a.tostring() for a in self.list]
> - cPickle.dump((l, self.object_cache), w, -1)
> - w.close()
> - os._exit(0)
> - else:
> - # Parent process
> - w.close()
> - (l, self.object_cache) = cPickle.load(r)
> - r.close()
> - os.waitpid(pid, 0)
> - self.list = []
> - for s in l:
> - a = array.array("H")
> - a.fromstring(s)
> - self.list.append(a)
> -
> -
> - def get_generator(self):
> + def get_dicts(self, node=None, ctx=[], content=[], shortname=[], dep=[]):
> """
> Generate dictionaries from the code parsed so far. This should
> - probably be called after parsing something.
> + be called after parsing something.
>
> @return: A dict generator.
> """
> - for a in self.list:
> - name, shortname, depend, content = _array_get_all(a,
> - self.object_cache)
> - dict = {"name": name, "shortname": shortname, "depend": depend}
> - self._apply_content_to_dict(dict, content)
> - yield dict
> -
> -
> - def get_list(self):
> - """
> - Generate a list of dictionaries from the code parsed so far.
> - This should probably be called after parsing something.
> + def apply_ops_to_dict(d, content):
> + for filename, linenum, s in content:
> + op_found = None
> + op_pos = len(s)
> + for op in ops:
> + if op in s:
> + pos = s.index(op)
> + if pos < op_pos:
> + op_found = op
> + op_pos = pos
> + if not op_found:
> + continue
> + left, value = map(str.strip, s.split(op_found, 1))
> + if value and ((value[0] == '"' and value[-1] == '"') or
> + (value[0] == "'" and value[-1] == "'")):
> + value = value[1:-1]
> + filters_and_key = map(str.strip, left.split(":"))
> + for f in filters_and_key[:-1]:
> + if not Filter(f).match(ctx, ctx_set):
> + break
> + else:
> + key = filters_and_key[-1]
> + ops[op_found](d, key, value)
> +
> + def process_content(content, failed_filters):
> + # 1. Check that the filters in content are OK with the current
> + # context (ctx).
> + # 2. Move the parts of content that are still relevant into
> + # new_content and unpack conditional blocks if appropriate.
> + # For example, if an 'only' statement fully matches ctx, it
> + # becomes irrelevant and is not appended to new_content.
> + # If a conditional block fully matches, its contents are
> + # unpacked into new_content.
> + # 3. Move failed filters into failed_filters, so that next time we
> + # reach this node or one of its ancestors, we'll check those
> + # filters first.
> + for t in content:
> + filename, linenum, obj = t
> + if type(obj) is str:
> + new_content.append(t)
> + continue
> + elif type(obj) is OnlyFilter:
> + if not obj.might_match(ctx, ctx_set, labels):
> + self._debug(" filter did not pass: %r (%s:%s)",
> + obj.line, filename, linenum)
> + failed_filters.append(t)
> + return False
> + elif obj.match(ctx, ctx_set):
> + continue
> + elif type(obj) is NoFilter:
> + if obj.match(ctx, ctx_set):
> + self._debug(" filter did not pass: %r (%s:%s)",
> + obj.line, filename, linenum)
> + failed_filters.append(t)
> + return False
> + elif not obj.might_match(ctx, ctx_set, labels):
> + continue
> + elif type(obj) is Condition:
> + if obj.match(ctx, ctx_set):
> + self._debug(" conditional block matches: %r (%s:%s)",
> + obj.line, filename, linenum)
> + # Check and unpack the content inside this Condition
> + # object (note: the failed filters should go into
> + # new_internal_filters because we don't expect them to
> + # come from outside this node, even if the Condition
> + # itself was external)
> + if not process_content(obj.content,
> + new_internal_filters):
> + failed_filters.append(t)
> + return False
> + continue
> + elif not obj.might_match(ctx, ctx_set, labels):
> + continue
> + new_content.append(t)
> + return True
> +
> + def might_pass(failed_ctx,
> + failed_ctx_set,
> + failed_external_filters,
> + failed_internal_filters):
> + for t in failed_external_filters:
> + if t not in content:
> + return True
> + filename, linenum, filter = t
> + if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
> + labels):
> + return True
> + for t in failed_internal_filters:
> + filename, linenum, filter = t
> + if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
> + labels):
> + return True
> + return False
> +
> + def add_failed_case():
> + node.failed_cases.appendleft((ctx, ctx_set,
> + new_external_filters,
> + new_internal_filters))
> + if len(node.failed_cases) > num_failed_cases:
> + node.failed_cases.pop()
> +
> + node = node or self.node
> + # Update dep
> + for d in node.dep:
> + temp = ctx + [d]
> + dep = dep + [".".join([s for s in temp if s])]
> + # Update ctx
> + ctx = ctx + node.name
> + ctx_set = set(ctx)
> + labels = node.labels
> + # Get the current name
> + name = ".".join([s for s in ctx if s])
> + if node.name:
> + self._debug("checking out %r", name)
> + # Check previously failed filters
> + for i, failed_case in enumerate(node.failed_cases):
> + if not might_pass(*failed_case):
> + self._debug(" this subtree has failed before")
> + del node.failed_cases[i]
> + node.failed_cases.appendleft(failed_case)
> + return
> + # Check content and unpack it into new_content
> + new_content = []
> + new_external_filters = []
> + new_internal_filters = []
> + if (not process_content(node.content, new_internal_filters) or
> + not process_content(content, new_external_filters)):
> + add_failed_case()
> + return
> + # Update shortname
> + if node.append_to_shortname:
> + shortname = shortname + node.name
> + # Recurse into children
> + count = 0
> + for n in node.children:
> + for d in self.get_dicts(n, ctx, new_content, shortname, dep):
> + count += 1
> + yield d
> + # Reached leaf?
> + if not node.children:
> + self._debug(" reached leaf, returning it")
> + d = {"name": name, "dep": dep,
> + "shortname": ".".join([s for s in shortname if s])}
> + apply_ops_to_dict(d, new_content)
> + yield d
> + # If this node did not produce any dicts, remember the failed filters
> + # of its descendants
> + elif not count:
> + new_external_filters = []
> + new_internal_filters = []
> + for n in node.children:
> + (failed_ctx,
> + failed_ctx_set,
> + failed_external_filters,
> + failed_internal_filters) = n.failed_cases[0]
> + for obj in failed_internal_filters:
> + if obj not in new_internal_filters:
> + new_internal_filters.append(obj)
> + for obj in failed_external_filters:
> + if obj in content:
> + if obj not in new_external_filters:
> + new_external_filters.append(obj)
> + else:
> + if obj not in new_internal_filters:
> + new_internal_filters.append(obj)
> + add_failed_case()
>
> - @return: A list of dicts.
> - """
> - return list(self.get_generator())
>
> + def _debug(self, s, *args):
> + if self.debug:
> + s = "DEBUG: %s" % s
> + print s % args
>
> - def count(self, filter=".*"):
> - """
> - Return the number of dictionaries whose names match filter.
>
> - @param filter: A regular expression string.
> - """
> - exp = self._get_filter_regex(filter)
> - count = 0
> - for a in self.list:
> - name = _array_get_name(a, self.object_cache)
> - if exp.search(name):
> - count += 1
> - return count
> + def _warn(self, s, *args):
> + s = "WARNING: %s" % s
> + print s % args
>
>
> - def parse_variants(self, cr, list, subvariants=False, prev_indent=-1):
> + def _parse_variants(self, cr, node, prev_indent=-1):
> """
> - Read and parse lines from a configreader object until a line with an
> + Read and parse lines from a FileReader object until a line with an
> indent level lower than or equal to prev_indent is encountered.
>
> - @brief: Parse a 'variants' or 'subvariants' block from a configreader
> - object.
> - @param cr: configreader object to be parsed.
> - @param list: List of arrays to operate on.
> - @param subvariants: If True, parse in 'subvariants' mode;
> - otherwise parse in 'variants' mode.
> + @param cr: A FileReader/StrReader object.
> + @param node: A node to operate on.
> @param prev_indent: The indent level of the "parent" block.
> - @return: The resulting list of arrays.
> + @return: A node object.
> """
> - new_list = []
> + node4 = Node()
>
> while True:
> - pos = cr.tell()
> - (indented_line, line, indent) = cr.get_next_line()
> - if indent <= prev_indent:
> - cr.seek(pos)
> + line, indent, linenum = cr.get_next_line(prev_indent)
> + if not line:
> break
>
> - # Get name and dependencies
> - (name, depend) = map(str.strip, line.lstrip("- ").split(":"))
> + name, dep = map(str.strip, line.lstrip("- ").split(":"))
>
> - # See if name should be added to the 'shortname' field
> - add_to_shortname = not name.startswith("@")
> - name = name.lstrip("@")
> + node2 = Node()
> + node2.children = [node]
> + node2.labels = node.labels
>
> - # Store name and dependencies in cache and get their indices
> - n = self._store_str(name)
> - d = self._store_str(depend)
> + node3 = self._parse(cr, node2, prev_indent=indent)
> + node3.name = name.lstrip("@").split(".")
> + node3.dep = dep.replace(",", " ").split()
> + node3.append_to_shortname = not name.startswith("@")
>
> - # Make a copy of list
> - temp_list = [a[:] for a in list]
> + node4.children += [node3]
> + node4.labels.update(node3.labels)
> + node4.labels.update(node3.name)
>
> - if subvariants:
> - # If we're parsing 'subvariants', first modify the list
> - if add_to_shortname:
> - for a in temp_list:
> - _array_append_to_name_shortname_depend(a, n, d)
> - else:
> - for a in temp_list:
> - _array_append_to_name_depend(a, n, d)
> - temp_list = self.parse(cr, temp_list, restricted=True,
> - prev_indent=indent)
> - else:
> - # If we're parsing 'variants', parse before modifying the list
> - if self.debug:
> - _debug_print(indented_line,
> - "Entering variant '%s' "
> - "(variant inherits %d dicts)" %
> - (name, len(list)))
> - temp_list = self.parse(cr, temp_list, restricted=False,
> - prev_indent=indent)
> - if add_to_shortname:
> - for a in temp_list:
> - _array_prepend_to_name_shortname_depend(a, n, d)
> - else:
> - for a in temp_list:
> - _array_prepend_to_name_depend(a, n, d)
> + return node4
>
> - new_list += temp_list
>
> - return new_list
> -
> -
> - def parse(self, cr, list, restricted=False, prev_indent=-1):
> + def _parse(self, cr, node, prev_indent=-1):
> """
> - Read and parse lines from a configreader object until a line with an
> + Read and parse lines from a StrReader object until a line with an
> indent level lower than or equal to prev_indent is encountered.
>
> - @brief: Parse a configreader object.
> - @param cr: A configreader object.
> - @param list: A list of arrays to operate on (list is modified in
> - place and should not be used after the call).
> - @param restricted: If True, operate in restricted mode
> - (prohibit 'variants').
> + @param cr: A FileReader/StrReader object.
> + @param node: A Node or a Condition object to operate on.
> @param prev_indent: The indent level of the "parent" block.
> - @return: The resulting list of arrays.
> - @note: List is destroyed and should not be used after the call.
> - Only the returned list should be used.
> + @return: A node object.
> """
> - current_block = ""
> -
> while True:
> - pos = cr.tell()
> - (indented_line, line, indent) = cr.get_next_line()
> - if indent <= prev_indent:
> - cr.seek(pos)
> - self._append_content_to_arrays(list, current_block)
> + line, indent, linenum = cr.get_next_line(prev_indent)
> + if not line:
> break
>
> - len_list = len(list)
> -
> - # Parse assignment operators (keep lines in temporary buffer)
> - if "=" in line:
> - if self.debug and not restricted:
> - _debug_print(indented_line,
> - "Parsing operator (%d dicts in current "
> - "context)" % len_list)
> - current_block += line + "\n"
> - continue
> -
> - # Flush the temporary buffer
> - self._append_content_to_arrays(list, current_block)
> - current_block = ""
> -
> words = line.split()
>
> - # Parse 'no' and 'only' statements
> - if words[0] == "no" or words[0] == "only":
> - if len(words) <= 1:
> - continue
> - filters = map(self._get_filter_regex, words[1:])
> - filtered_list = []
> - if words[0] == "no":
> - for a in list:
> - name = _array_get_name(a, self.object_cache)
> - for filter in filters:
> - if filter.search(name):
> - break
> - else:
> - filtered_list.append(a)
> - if words[0] == "only":
> - for a in list:
> - name = _array_get_name(a, self.object_cache)
> - for filter in filters:
> - if filter.search(name):
> - filtered_list.append(a)
> - break
> - list = filtered_list
> - if self.debug and not restricted:
> - _debug_print(indented_line,
> - "Parsing no/only (%d dicts in current "
> - "context, %d remain)" %
> - (len_list, len(list)))
> - continue
> -
> # Parse 'variants'
> if line == "variants:":
> - # 'variants' not allowed in restricted mode
> - # (inside an exception or inside subvariants)
> - if restricted:
> - e_msg = "Using variants in this context is not allowed"
> - cr.raise_error(e_msg)
> - if self.debug and not restricted:
> - _debug_print(indented_line,
> - "Entering variants block (%d dicts in "
> - "current context)" % len_list)
> - list = self.parse_variants(cr, list, subvariants=False,
> - prev_indent=indent)
> - continue
> -
> - # Parse 'subvariants' (the block is parsed for each dict
> - # separately)
> - if line == "subvariants:":
> - if self.debug and not restricted:
> - _debug_print(indented_line,
> - "Entering subvariants block (%d dicts in "
> - "current context)" % len_list)
> - new_list = []
> - # Remember current position
> - pos = cr.tell()
> - # Read the lines in any case
> - self.parse_variants(cr, [], subvariants=True,
> - prev_indent=indent)
> - # Iterate over the list...
> - for index in xrange(len(list)):
> - # Revert to initial position in this 'subvariants' block
> - cr.seek(pos)
> - # Everything inside 'subvariants' should be parsed in
> - # restricted mode
> - new_list += self.parse_variants(cr, list[index:index+1],
> - subvariants=True,
> - prev_indent=indent)
> - list = new_list
> + # 'variants' is not allowed inside a conditional block
> + if isinstance(node, Condition):
> + raise ValueError("'variants' is not allowed inside a "
> + "conditional block (%s:%s)" %
> + (cr.filename, linenum))
> + node = self._parse_variants(cr, node, prev_indent=indent)
> continue
>
> # Parse 'include' statements
> if words[0] == "include":
> - if len(words) <= 1:
> + if len(words) < 2:
> + self._warn("%r (%s:%s): missing parameter. What are you "
> + "including?", line, cr.filename, linenum)
> continue
> - if self.debug and not restricted:
> - _debug_print(indented_line, "Entering file %s" % words[1])
> -
> - cur_filename = cr.real_filename()
> - if cur_filename is None:
> - cr.raise_error("'include' is valid only when parsing a file")
> -
> - filename = os.path.join(os.path.dirname(cur_filename),
> - words[1])
> - if not os.path.exists(filename):
> - cr.raise_error("Cannot include %s -- file not found" % (filename))
> -
> - str = open(filename).read()
> - list = self.parse(configreader(filename, str), list, restricted)
> - if self.debug and not restricted:
> - _debug_print("", "Leaving file %s" % words[1])
> + if not isinstance(cr, FileReader):
> + self._warn("%r (%s:%s): cannot include because no file is "
> + "currently open", line, cr.filename, linenum)
> + continue
> + filename = os.path.join(os.path.dirname(cr.filename), words[1])
> + if not os.path.isfile(filename):
> + self._warn("%r (%s:%s): file doesn't exist or is not a "
> + "regular file", line, cr.filename, linenum)
> + continue
> + node = self._parse(FileReader(filename), node)
> + continue
>
> + # Parse 'only' and 'no' filters
> + if words[0] in ("only", "no"):
> + if len(words) < 2:
> + self._warn("%r (%s:%s): missing parameter", line,
> + cr.filename, linenum)
> + continue
> + if words[0] == "only":
> + node.content += [(cr.filename, linenum, OnlyFilter(line))]
> + elif words[0] == "no":
> + node.content += [(cr.filename, linenum, NoFilter(line))]
> continue
>
> - # Parse multi-line exceptions
> - # (the block is parsed for each dict separately)
> + # Parse conditional blocks
> if line.endswith(":"):
> - if self.debug and not restricted:
> - _debug_print(indented_line,
> - "Entering multi-line exception block "
> - "(%d dicts in current context outside "
> - "exception)" % len_list)
> - line = line[:-1]
> - new_list = []
> - # Remember current position
> - pos = cr.tell()
> - # Read the lines in any case
> - self.parse(cr, [], restricted=True, prev_indent=indent)
> - # Iterate over the list...
> - exp = self._get_filter_regex(line)
> - for index in xrange(len(list)):
> - name = _array_get_name(list[index], self.object_cache)
> - if exp.search(name):
> - # Revert to initial position in this exception block
> - cr.seek(pos)
> - # Everything inside an exception should be parsed in
> - # restricted mode
> - new_list += self.parse(cr, list[index:index+1],
> - restricted=True,
> - prev_indent=indent)
> - else:
> - new_list.append(list[index])
> - list = new_list
> + cond = Condition(line)
> + self._parse(cr, cond, prev_indent=indent)
> + node.content += [(cr.filename, linenum, cond)]
> continue
>
> - return list
> + node.content += [(cr.filename, linenum, line)]
> + continue
>
> -
> - def _get_filter_regex(self, filter):
> - """
> - Return a regex object corresponding to a given filter string.
> -
> - All regular expressions given to the parser are passed through this
> - function first. Its purpose is to make them more specific and better
> - suited to match dictionary names: it forces simple expressions to match
> - only between dots or at the beginning or end of a string. For example,
> - the filter 'foo' will match 'foo.bar' but not 'foobar'.
> - """
> - try:
> - return self.regex_cache[filter]
> - except KeyError:
> - exp = re.compile(r"(\.|^)(%s)(\.|$)" % filter)
> - self.regex_cache[filter] = exp
> - return exp
> -
> -
> - def _store_str(self, str):
> - """
> - Store str in the internal object cache, if it isn't already there, and
> - return its identifying index.
> -
> - @param str: String to store.
> - @return: The index of str in the object cache.
> - """
> - try:
> - return self.object_cache_indices[str]
> - except KeyError:
> - self.object_cache.append(str)
> - index = len(self.object_cache) - 1
> - self.object_cache_indices[str] = index
> - return index
> -
> -
> - def _append_content_to_arrays(self, list, content):
> - """
> - Append content (config code containing assignment operations) to a list
> - of arrays.
> -
> - @param list: List of arrays to operate on.
> - @param content: String containing assignment operations.
> - """
> - if content:
> - str_index = self._store_str(content)
> - for a in list:
> - _array_append_to_content(a, str_index)
> -
> -
> - def _apply_content_to_dict(self, dict, content):
> - """
> - Apply the operations in content (config code containing assignment
> - operations) to a dict.
> -
> - @param dict: Dictionary to operate on. Must have 'name' key.
> - @param content: String containing assignment operations.
> - """
> - for line in content.splitlines():
> - op_found = None
> - op_pos = len(line)
> - for op in ops:
> - pos = line.find(op)
> - if pos >= 0 and pos < op_pos:
> - op_found = op
> - op_pos = pos
> - if not op_found:
> - continue
> - (left, value) = map(str.strip, line.split(op_found, 1))
> - if value and ((value[0] == '"' and value[-1] == '"') or
> - (value[0] == "'" and value[-1] == "'")):
> - value = value[1:-1]
> - filters_and_key = map(str.strip, left.split(":"))
> - filters = filters_and_key[:-1]
> - key = filters_and_key[-1]
> - for filter in filters:
> - exp = self._get_filter_regex(filter)
> - if not exp.search(dict["name"]):
> - break
> - else:
> - ops[op_found](dict, key, value)
> + return node
>
>
> # Assignment operators
>
> -def _op_set(dict, key, value):
> - dict[key] = value
> +def _op_set(d, key, value):
> + d[key] = value
>
>
> -def _op_append(dict, key, value):
> - dict[key] = dict.get(key, "") + value
> +def _op_append(d, key, value):
> + d[key] = d.get(key, "") + value
>
>
> -def _op_prepend(dict, key, value):
> - dict[key] = value + dict.get(key, "")
> +def _op_prepend(d, key, value):
> + d[key] = value + d.get(key, "")
>
>
> -def _op_regex_set(dict, exp, value):
> +def _op_regex_set(d, exp, value):
> exp = re.compile("^(%s)$" % exp)
> - for key in dict:
> + for key in d:
> if exp.match(key):
> - dict[key] = value
> + d[key] = value
>
>
> -def _op_regex_append(dict, exp, value):
> +def _op_regex_append(d, exp, value):
> exp = re.compile("^(%s)$" % exp)
> - for key in dict:
> + for key in d:
> if exp.match(key):
> - dict[key] += value
> + d[key] += value
>
>
> -def _op_regex_prepend(dict, exp, value):
> +def _op_regex_prepend(d, exp, value):
> exp = re.compile("^(%s)$" % exp)
> - for key in dict:
> + for key in d:
> if exp.match(key):
> - dict[key] = value + dict[key]
> -
> + d[key] = value + d[key]
>
> -ops = {
> - "=": _op_set,
> - "+=": _op_append,
> - "<=": _op_prepend,
> - "?=": _op_regex_set,
> - "?+=": _op_regex_append,
> - "?<=": _op_regex_prepend,
> -}
> -
> -
> -# Misc functions
> -
> -def _debug_print(str1, str2=""):
> - """
> - Nicely print two strings and an arrow.
>
> - @param str1: First string.
> - @param str2: Second string.
> - """
> - if str2:
> - str = "%-50s ---> %s" % (str1, str2)
> - else:
> - str = str1
> - logging.debug(str)
> +ops = {"=": _op_set,
> + "+=": _op_append,
> + "<=": _op_prepend,
> + "?=": _op_regex_set,
> + "?+=": _op_regex_append,
> + "?<=": _op_regex_prepend}
>
>
> -# configreader
> +# StrReader and FileReader
>
> -class configreader:
> +class StrReader(object):
> """
> - Preprocess an input string and provide file-like services.
> - This is intended as a replacement for the file and StringIO classes,
> - whose readline() and/or seek() methods seem to be slow.
> + Preprocess an input string for easy reading.
> """
> -
> - def __init__(self, filename, str, real_file=True):
> + def __init__(self, s):
> """
> Initialize the reader.
>
> - @param filename: the filename we're parsing
> - @param str: The string to parse.
> - @param real_file: Indicates if filename represents a real file. Defaults to True.
> + @param s: The string to parse.
> """
> - self.filename = filename
> - self.is_real_file = real_file
> - self.line_index = 0
> - self.lines = []
> - self.real_number = []
> - for num, line in enumerate(str.splitlines()):
> + self.filename = "<string>"
> + self._lines = []
> + self._line_index = 0
> + for linenum, line in enumerate(s.splitlines()):
> line = line.rstrip().expandtabs()
> - stripped_line = line.strip()
> + stripped_line = line.lstrip()
> indent = len(line) - len(stripped_line)
> if (not stripped_line
> or stripped_line.startswith("#")
> or stripped_line.startswith("//")):
> continue
> - self.lines.append((line, stripped_line, indent))
> - self.real_number.append(num + 1)
> -
> -
> - def real_filename(self):
> - """Returns the filename we're reading, in case it is a real file
> -
> - @returns the filename we are parsing, or None in case we're not parsing a real file
> - """
> - if self.is_real_file:
> - return self.filename
> -
> - def get_next_line(self):
> - """
> - Get the next non-empty, non-comment line in the string.
> + self._lines.append((stripped_line, indent, linenum + 1))
>
> - @param file: File like object.
> - @return: (line, stripped_line, indent), where indent is the line's
> - indent level or -1 if no line is available.
> - """
> - try:
> - if self.line_index < len(self.lines):
> - return self.lines[self.line_index]
> - else:
> - return (None, None, -1)
> - finally:
> - self.line_index += 1
>
> -
> - def tell(self):
> - """
> - Return the current line index.
> - """
> - return self.line_index
> -
> -
> - def seek(self, index):
> - """
> - Set the current line index.
> + def get_next_line(self, prev_indent):
> """
> - self.line_index = index
> + Get the next non-empty, non-comment line in the string, whose
> + indentation level is higher than prev_indent.
>
> - def raise_error(self, msg):
> - """Raise an error related to the last line returned by get_next_line()
> + @param prev_indent: The indentation level of the previous block.
> + @return: (line, indent, linenum), where indent is the line's
> + indentation level. If no line is available, (None, -1, -1) is
> + returned.
> """
> - if self.line_index == 0: # nothing was read. shouldn't happen, but...
> - line_id = 'BEGIN'
> - elif self.line_index >= len(self.lines): # past EOF
> - line_id = 'EOF'
> - else:
> - # line_index is the _next_ line. get the previous one
> - line_id = str(self.real_number[self.line_index-1])
> - raise error.AutotestError("%s:%s: %s" % (self.filename, line_id, msg))
> -
> -
> -# Array structure:
> -# ----------------
> -# The first 4 elements contain the indices of the 4 segments.
> -# a[0] -- Index of beginning of 'name' segment (always 4).
> -# a[1] -- Index of beginning of 'shortname' segment.
> -# a[2] -- Index of beginning of 'depend' segment.
> -# a[3] -- Index of beginning of 'content' segment.
> -# The next elements in the array comprise the aforementioned segments:
> -# The 'name' segment begins with a[a[0]] and ends with a[a[1]-1].
> -# The 'shortname' segment begins with a[a[1]] and ends with a[a[2]-1].
> -# The 'depend' segment begins with a[a[2]] and ends with a[a[3]-1].
> -# The 'content' segment begins with a[a[3]] and ends at the end of the array.
> -
> -# The following functions append/prepend to various segments of an array.
> -
> -def _array_append_to_name_shortname_depend(a, name, depend):
> - a.insert(a[1], name)
> - a.insert(a[2] + 1, name)
> - a.insert(a[3] + 2, depend)
> - a[1] += 1
> - a[2] += 2
> - a[3] += 3
> -
> -
> -def _array_prepend_to_name_shortname_depend(a, name, depend):
> - a[1] += 1
> - a[2] += 2
> - a[3] += 3
> - a.insert(a[0], name)
> - a.insert(a[1], name)
> - a.insert(a[2], depend)
> -
> + if self._line_index >= len(self._lines):
> + return None, -1, -1
> + line, indent, linenum = self._lines[self._line_index]
> + if indent <= prev_indent:
> + return None, -1, -1
> + self._line_index += 1
> + return line, indent, linenum
>
> -def _array_append_to_name_depend(a, name, depend):
> - a.insert(a[1], name)
> - a.insert(a[3] + 1, depend)
> - a[1] += 1
> - a[2] += 1
> - a[3] += 2
>
> -
> -def _array_prepend_to_name_depend(a, name, depend):
> - a[1] += 1
> - a[2] += 1
> - a[3] += 2
> - a.insert(a[0], name)
> - a.insert(a[2], depend)
> -
> -
> -def _array_append_to_content(a, content):
> - a.append(content)
> -
> -
> -def _array_get_name(a, object_cache):
> - """
> - Return the name of a dictionary represented by a given array.
> -
> - @param a: Array representing a dictionary.
> - @param object_cache: A list of strings referenced by elements in the array.
> +class FileReader(StrReader):
> """
> - return ".".join([object_cache[i] for i in a[a[0]:a[1]]])
> -
> -
> -def _array_get_all(a, object_cache):
> + Preprocess an input file for easy reading.
> """
> - Return a 4-tuple containing all the data stored in a given array, in a
> - format that is easy to turn into an actual dictionary.
> + def __init__(self, filename):
> + """
> + Initialize the reader.
>
> - @param a: Array representing a dictionary.
> - @param object_cache: A list of strings referenced by elements in the array.
> - @return: A 4-tuple: (name, shortname, depend, content), in which all
> - members are strings except depend which is a list of strings.
> - """
> - name = ".".join([object_cache[i] for i in a[a[0]:a[1]]])
> - shortname = ".".join([object_cache[i] for i in a[a[1]:a[2]]])
> - content = "".join([object_cache[i] for i in a[a[3]:]])
> - depend = []
> - prefix = ""
> - for n, d in zip(a[a[0]:a[1]], a[a[2]:a[3]]):
> - for dep in object_cache[d].split():
> - depend.append(prefix + dep)
> - prefix += object_cache[n] + "."
> - return name, shortname, depend, content
> + @parse filename: The name of the input file.
> + """
> + StrReader.__init__(self, open(filename).read())
> + self.filename = filename
>
>
> if __name__ == "__main__":
> - parser = optparse.OptionParser("usage: %prog [options] [filename]")
> - parser.add_option('--verbose', dest="debug", action='store_true',
> - help='include debug messages in console output')
> + parser = optparse.OptionParser("usage: %prog [options] <filename>")
> + parser.add_option("-v", "--verbose", dest="debug", action="store_true",
> + help="include debug messages in console output")
> + parser.add_option("-f", "--fullname", dest="fullname", action="store_true",
> + help="show full dict names instead of short names")
> + parser.add_option("-c", "--contents", dest="contents", action="store_true",
> + help="show dict contents")
>
> options, args = parser.parse_args()
> - debug = options.debug
> - if args:
> - filenames = args
> - else:
> - filenames = [os.path.join(os.path.dirname(sys.argv[0]), "tests.cfg")]
> -
> - # Here we configure the stand alone program to use the autotest
> - # logging system.
> - logging_manager.configure_logging(kvm_utils.KvmLoggingConfig(),
> - verbose=debug)
> - cfg = config(debug=debug)
> - for fn in filenames:
> - cfg.parse_file(fn)
> - dicts = cfg.get_generator()
> - for i, dict in enumerate(dicts):
> - print "Dictionary #%d:" % (i)
> - keys = dict.keys()
> - keys.sort()
> - for key in keys:
> - print " %s = %s" % (key, dict[key])
> + if not args:
> + parser.error("filename required")
> +
> + c = Parser(args[0], debug=options.debug)
> + for i, d in enumerate(c.get_dicts()):
> + if options.fullname:
> + print "dict %4d: %s" % (i + 1, d["name"])
> + else:
> + print "dict %4d: %s" % (i + 1, d["shortname"])
> + if options.contents:
> + keys = d.keys()
> + keys.sort()
> + for key in keys:
> + print " %s = %s" % (key, d[key])
> diff --git a/client/tests/kvm/kvm_scheduler.py b/client/tests/kvm/kvm_scheduler.py
> index 95282e4..b96bb32 100644
> --- a/client/tests/kvm/kvm_scheduler.py
> +++ b/client/tests/kvm/kvm_scheduler.py
> @@ -63,7 +63,6 @@ class scheduler:
> test_index = int(cmd[1])
> test = self.tests[test_index].copy()
> test.update(self_dict)
> - test = kvm_utils.get_sub_pool(test, index, self.num_workers)
> test_iterations = int(test.get("iterations", 1))
> status = run_test_func("kvm", params=test,
> tag=test.get("shortname"),
> @@ -129,7 +128,7 @@ class scheduler:
> # If the test failed, mark all dependent tests as "failed" too
> if not status:
> for i, other_test in enumerate(self.tests):
> - for dep in other_test.get("depend", []):
> + for dep in other_test.get("dep", []):
> if dep in test["name"]:
> test_status[i] = "fail"
>
> @@ -154,7 +153,7 @@ class scheduler:
> continue
> # Make sure the test's dependencies are satisfied
> dependencies_satisfied = True
> - for dep in test["depend"]:
> + for dep in test["dep"]:
> dependencies = [j for j, t in enumerate(self.tests)
> if dep in t["name"]]
> bad_status_deps = [j for j in dependencies
> @@ -200,14 +199,14 @@ class scheduler:
> used_mem[worker] = test_used_mem
> # Assign all related tests to this worker
> for j, other_test in enumerate(self.tests):
> - for other_dep in other_test["depend"]:
> + for other_dep in other_test["dep"]:
> # All tests that depend on this test
> if other_dep in test["name"]:
> test_worker[j] = worker
> break
> # ... and all tests that share a dependency
> # with this test
> - for dep in test["depend"]:
> + for dep in test["dep"]:
> if dep in other_dep or other_dep in dep:
> test_worker[j] = worker
> break
> diff --git a/client/tests/kvm/kvm_utils.py b/client/tests/kvm/kvm_utils.py
> index 44ebb88..9e25a0a 100644
> --- a/client/tests/kvm/kvm_utils.py
> +++ b/client/tests/kvm/kvm_utils.py
> @@ -1101,7 +1101,7 @@ def run_tests(test_list, job):
> if dict.get("skip") == "yes":
> continue
> dependencies_satisfied = True
> - for dep in dict.get("depend"):
> + for dep in dict.get("dep"):
> for test_name in status_dict.keys():
> if not dep in test_name:
> continue
> diff --git a/client/tests/kvm/tests.cfg.sample b/client/tests/kvm/tests.cfg.sample
> index bde7aba..4b3b965 100644
> --- a/client/tests/kvm/tests.cfg.sample
> +++ b/client/tests/kvm/tests.cfg.sample
> @@ -18,10 +18,9 @@ include cdkeys.cfg
> image_name(_.*)? ?<= /tmp/kvm_autotest_root/images/
> cdrom(_.*)? ?<= /tmp/kvm_autotest_root/
> floppy ?<= /tmp/kvm_autotest_root/
> -Linux:
> - unattended_install:
> - kernel ?<= /tmp/kvm_autotest_root/
> - initrd ?<= /tmp/kvm_autotest_root/
> +Linux..unattended_install:
> + kernel ?<= /tmp/kvm_autotest_root/
> + initrd ?<= /tmp/kvm_autotest_root/
>
> # Here are the test sets variants. The variant 'qemu_kvm_windows_quick' is
> # fully commented, the following ones have comments only on noteworthy points
> @@ -49,7 +48,7 @@ variants:
> # Operating system choice
> only Win7.64
> # Subtest choice. You can modify that line to add more subtests
> - only unattended_install.cdrom boot shutdown
> + only unattended_install.cdrom, boot, shutdown
>
> # Runs qemu, f14 64 bit guest OS, install, boot, shutdown
> - @qemu_f14_quick:
> @@ -65,7 +64,7 @@ variants:
> only no_pci_assignable
> only smallpages
> only Fedora.14.64
> - only unattended_install.cdrom boot shutdown
> + only unattended_install.cdrom, boot, shutdown
> # qemu needs -enable-kvm on the cmdline
> extra_params += ' -enable-kvm'
>
> @@ -81,7 +80,7 @@ variants:
> only no_pci_assignable
> only smallpages
> only Fedora.14.64
> - only unattended_install.cdrom boot shutdown
> + only unattended_install.cdrom, boot, shutdown
>
> # You may provide information about the DTM server for WHQL tests here:
> #whql:
> diff --git a/client/tests/kvm/tests_base.cfg.sample b/client/tests/kvm/tests_base.cfg.sample
> index 80362db..e65bed2 100644
> --- a/client/tests/kvm/tests_base.cfg.sample
> +++ b/client/tests/kvm/tests_base.cfg.sample
> @@ -1722,8 +1722,8 @@ variants:
>
> # Windows section
> - @Windows:
> - no autotest linux_s3 vlan ioquit unattended_install.(url|nfs|remote_ks)
> - no jumbo nicdriver_unload nic_promisc multicast mac_change ethtool clock_getres
> + no autotest, linux_s3, vlan, ioquit, unattended_install.url, unattended_install.nfs, unattended_install.remote_ks
> + no jumbo, nicdriver_unload, nic_promisc, multicast, mac_change, ethtool, clock_getres
>
> shutdown_command = shutdown /s /f /t 0
> reboot_command = shutdown /r /f /t 0
> @@ -1747,7 +1747,7 @@ variants:
> mem_chk_cmd = wmic memphysical
> mem_chk_cur_cmd = wmic memphysical
>
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> timeout = 7200
> finish_program = deps/finish.exe
> cdroms += " winutils"
> @@ -1857,7 +1857,7 @@ variants:
> steps = WinXP-32.steps
> setup:
> steps = WinXP-32-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/WindowsXP-sp2-vlk.iso
> md5sum_cd1 = 743450644b1d9fe97b3cf379e22dceb0
> md5sum_1m_cd1 = b473bf75af2d1269fec8958cf0202bfd
> @@ -1890,7 +1890,7 @@ variants:
> steps = WinXP-64.steps
> setup:
> steps = WinXP-64-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/WindowsXP-64.iso
> md5sum_cd1 = 8d3f007ec9c2060cec8a50ee7d7dc512
> md5sum_1m_cd1 = e812363ff427effc512b7801ee70e513
> @@ -1928,7 +1928,7 @@ variants:
> steps = Win2003-32.steps
> setup:
> steps = Win2003-32-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/Windows2003_r2_VLK.iso
> md5sum_cd1 = 03e921e9b4214773c21a39f5c3f42ef7
> md5sum_1m_cd1 = 37c2fdec15ac4ec16aa10fdfdb338aa3
> @@ -1960,7 +1960,7 @@ variants:
> steps = Win2003-64.steps
> setup:
> steps = Win2003-64-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/Windows2003-x64.iso
> md5sum_cd1 = 5703f87c9fd77d28c05ffadd3354dbbd
> md5sum_1m_cd1 = 439393c384116aa09e08a0ad047dcea8
> @@ -2008,7 +2008,7 @@ variants:
> steps = Win-Vista-32.steps
> setup:
> steps = WinVista-32-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/WindowsVista-32.iso
> md5sum_cd1 = 1008f323d5170c8e614e52ccb85c0491
> md5sum_1m_cd1 = c724e9695da483bc0fd59e426eaefc72
> @@ -2025,7 +2025,7 @@ variants:
>
> - sp2:
> image_name += -sp2-32
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_vista_with_sp2_x86_dvd_342266.iso
> md5sum_cd1 = 19ca90a425667812977bab6f4ce24175
> md5sum_1m_cd1 = 89c15020e0e6125be19acf7a2e5dc614
> @@ -2059,7 +2059,7 @@ variants:
> steps = Win-Vista-64.steps
> setup:
> steps = WinVista-64-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/WindowsVista-64.iso
> md5sum_cd1 = 11e2010d857fffc47813295e6be6d58d
> md5sum_1m_cd1 = 0947bcd5390546139e25f25217d6f165
> @@ -2076,7 +2076,7 @@ variants:
>
> - sp2:
> image_name += -sp2-64
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_vista_sp2_x64_dvd_342267.iso
> md5sum_cd1 = a1c024d7abaf34bac3368e88efbc2574
> md5sum_1m_cd1 = 3d84911a80f3df71d1026f7adedc2181
> @@ -2112,7 +2112,7 @@ variants:
> steps = Win2008-32.steps
> setup:
> steps = Win2008-32-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/Windows2008-x86.iso
> md5sum=0bfca49f0164de0a8eba236ced47007d
> md5sum_1m=07d7f5006393f74dc76e6e2e943e2440
> @@ -2127,7 +2127,7 @@ variants:
>
> - sp2:
> image_name += -sp2-32
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_server_2008_datacenter_enterprise_standard_sp2_x86_dvd_342333.iso
> md5sum_cd1 = b9201aeb6eef04a3c573d036a8780bdf
> md5sum_1m_cd1 = b7a9d42e55ea1e85105a3a6ad4da8e04
> @@ -2156,7 +2156,7 @@ variants:
> passwd = 1q2w3eP
> setup:
> steps = Win2008-64-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/Windows2008-x64.iso
> md5sum=27c58cdb3d620f28c36333a5552f271c
> md5sum_1m=efdcc11d485a1ef9afa739cb8e0ca766
> @@ -2171,7 +2171,7 @@ variants:
>
> - sp2:
> image_name += -sp2-64
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_server_2008_datacenter_enterprise_standard_sp2_x64_dvd_342336.iso
> md5sum_cd1 = e94943ef484035b3288d8db69599a6b5
> md5sum_1m_cd1 = ee55506823d0efffb5532ddd88a8e47b
> @@ -2188,7 +2188,7 @@ variants:
>
> - r2:
> image_name += -r2-64
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_server_2008_r2_standard_enterprise_datacenter_and_web_x64_dvd_x15-59754.iso
> md5sum_cd1 = 0207ef392c60efdda92071b0559ca0f9
> md5sum_1m_cd1 = a5a22ce25008bd7109f6d830d627e3ed
> @@ -2216,7 +2216,7 @@ variants:
> variants:
> - 32:
> image_name += -32
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_7_ultimate_x86_dvd_x15-65921.iso
> md5sum_cd1 = d0b8b407e8a3d4b75ee9c10147266b89
> md5sum_1m_cd1 = 2b0c2c22b1ae95065db08686bf83af93
> @@ -2249,7 +2249,7 @@ variants:
> steps = Win7-64.steps
> setup:
> steps = Win7-64-rss.steps
> - unattended_install.cdrom|whql.support_vm_install:
> + unattended_install.cdrom, whql.support_vm_install:
> cdrom_cd1 = isos/windows/en_windows_7_ultimate_x64_dvd_x15-65922.iso
> md5sum_cd1 = f43d22e4fb07bf617d573acd8785c028
> md5sum_1m_cd1 = b44d8cf99dbed2a5cb02765db8dfd48f
> @@ -2329,7 +2329,7 @@ variants:
> md5sum_cd1 = 9fae22f2666369968a76ef59e9a81ced
>
>
> -whql.support_vm_install|whql.client_install.support_vm:
> +whql.support_vm_install, whql.client_install.support_vm:
> image_name += -supportvm
>
>
> @@ -2352,7 +2352,7 @@ variants:
> drive_format=virtio
>
>
> -virtio_net|virtio_blk|e1000|balloon_check:
> +virtio_net, virtio_blk, e1000, balloon_check:
> only Fedora.11 Fedora.12 Fedora.13 Fedora.14 RHEL.5 RHEL.6 OpenSUSE.11 SLES.11 Ubuntu-8.10-server
> # only WinXP Win2003 Win2008 WinVista Win7 Fedora.11 Fedora.12 Fedora.13 Fedora.14 RHEL.5 RHEL.6 OpenSUSE.11 SLES.11 Ubuntu-8.10-server
>
> @@ -2365,15 +2365,9 @@ variants:
> check_image = yes
> - vmdk:
> no ioquit
> - only Fedora Ubuntu Windows
> - only smp2
> - only rtl8139
> image_format = vmdk
> - raw:
> no ioquit
> - only Fedora Ubuntu Windows
> - only smp2
> - only rtl8139
> image_format = raw
>
>
> --
> 1.7.3.4
>
> --
> To unsubscribe from this list: send the line "unsubscribe kvm" in
> the body of a message to majordomo@vger.kernel.org
> More majordomo info at http://vger.kernel.org/majordomo-info.html

--
Ryan Harper
Software Engineer; Linux Technology Center
IBM Corp., Austin, Tx
ryanh@us.ibm.com