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

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



* Michael Goldish <mgoldish@xxxxxxxxxx> [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@xxxxxxxxxx>
> Signed-off-by: Uri Lublin <ulublin@xxxxxxxxxx>
> ---
>  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@xxxxxxxxxxxxxxx
> More majordomo info at  http://vger.kernel.org/majordomo-info.html

-- 
Ryan Harper
Software Engineer; Linux Technology Center
IBM Corp., Austin, Tx
ryanh@xxxxxxxxxx
--
To unsubscribe from this list: send the line "unsubscribe kvm" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [KVM ARM]     [KVM ia64]     [KVM ppc]     [Virtualization Tools]     [Spice Development]     [Libvirt]     [Libvirt Users]     [Linux USB Devel]     [Linux Audio Users]     [Yosemite Questions]     [Linux Kernel]     [Linux SCSI]     [XFree86]
  Powered by Linux