public inbox for kvm@vger.kernel.org
 help / color / mirror / Atom feed
From: Cleber Rosa <crosa@redhat.com>
To: Michael Goldish <mgoldish@redhat.com>
Cc: autotest@test.kernel.org, Uri Lublin <ulublin@redhat.com>,
	kvm@vger.kernel.org
Subject: Re: [KVM-AUTOTEST PATCH] KVM test: refactor kvm_config.py
Date: Wed, 09 Feb 2011 00:56:31 -0200	[thread overview]
Message-ID: <4D52025F.5090108@redhat.com> (raw)
In-Reply-To: <1297216207-28375-1-git-send-email-mgoldish@redhat.com>

Top posting to make the congratulations reach you sooner: this was very 
much anticipated and very much appreciated!

Saving 2 minutes on each test job run is great! But going from 2 minutes 
to (almost) nil on every config experimentation is amazing!

only kudos..congrats..cheers

On 02/08/2011 11:50 PM, Michael Goldish wrote:
> 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.
>
> Instead of regular expressions in the filters, the following syntax is used:
>
> , means OR
> .. means AND
> . means IMMEDIATELY-FOLLOWED-BY
>
> 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)
>
> '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
>
>

  reply	other threads:[~2011-02-09  2:56 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2011-02-09  1:50 [KVM-AUTOTEST PATCH] KVM test: refactor kvm_config.py Michael Goldish
2011-02-09  2:56 ` Cleber Rosa [this message]
2011-02-09  9:28 ` Avi Kivity
2011-02-09 10:07   ` Michael Goldish
2011-02-09 10:19     ` Avi Kivity
2011-02-10  1:18   ` Amos Kong
2011-02-10 12:42     ` Lucas Meneghel Rodrigues
2011-02-09 16:06 ` Ryan Harper
2011-02-09 16:21   ` Eduardo Habkost
2011-02-09 23:31     ` [Autotest] " Ryan Harper
2011-02-10  9:14       ` Michael Goldish
2011-02-10 10:34         ` [Autotest] " Avi Kivity
2011-02-10 10:46           ` Michael Goldish
2011-02-10 10:47             ` Avi Kivity
2011-02-10 10:55               ` Michael Goldish
2011-02-10 10:57                 ` [Autotest] " Michael Goldish
2011-02-10 11:03                   ` Avi Kivity
2011-02-10 11:46                     ` Michael Goldish
2011-02-10 13:45                     ` Eduardo Habkost

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=4D52025F.5090108@redhat.com \
    --to=crosa@redhat.com \
    --cc=autotest@test.kernel.org \
    --cc=kvm@vger.kernel.org \
    --cc=mgoldish@redhat.com \
    --cc=ulublin@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox