sanitycheck: allow for more expressive filtering in testcase.ini

The old 'config_whitelist' directive in testcase.ini has been removed.
We use the new expr_parser module to parse a 'filter' directive which
is a boolean expression. This gives a great deal more flexibility
in how tests can be filtered.

To keep the tree bisectable, use of config_whitelist in testcase.ini
converted to the new expression language.

Change-Id: I0617319818c5559c0f0569d2fa73d09b681cac51
Signed-off-by: Andrew Boie <andrew.p.boie@intel.com>
diff --git a/scripts/sanitycheck b/scripts/sanitycheck
index a22b841..1d849be 100755
--- a/scripts/sanitycheck
+++ b/scripts/sanitycheck
@@ -58,11 +58,57 @@
   platform_exclude = <list of platforms>
     Set of platforms that this test case should not run on.
 
-  config_whitelist = <list of config options>
-    Config options can either be config names like CONFIG_FOO which
-    match if the configuration is defined to any value, or key/value
-    pairs like CONFIG_FOO=bar which match if it is set to a specific
-    value. May prepend a '!' to invert the match.
+  filter = <expression>
+    Filter whether the testcase should be run by evaluating an expression
+    against an environment containing the following values:
+
+    { ARCH : <architecture>,
+      PLATFORM : <platform>,
+      <all CONFIG_* key/value pairs in the test's generated defconfig>
+    }
+
+    The grammar for the expression language is as follows:
+
+    expression ::= expression "and" expression
+                 | expression "or" expression
+                 | "not" expression
+                 | "(" expression ")"
+                 | symbol "==" constant
+                 | symbol "!=" constant
+                 | symbol "<" number
+                 | symbol ">" number
+                 | symbol ">=" number
+                 | symbol "<=" number
+                 | symbol "in" list
+                 | symbol
+
+    list ::= "[" list_contents "]"
+
+    list_contents ::= constant
+                    | list_contents "," constant
+
+    constant ::= number
+               | string
+
+
+    For the case where expression ::= symbol, it evaluates to true
+    if the symbol is defined to a non-empty string.
+
+    Operator precedence, starting from lowest to highest:
+
+        or (left associative)
+        and (left associative)
+        not (right associative)
+        all comparison operators (non-associative)
+
+    arch_whitelist, arch_exclude, platform_whitelist, platform_exclude
+    are all syntactic sugar for these expressions. For instance
+
+        arch_exclude = x86 arc
+
+    Is the same as:
+
+        filter = not ARCH in ["x86", "arc"]
 
 Architectures and platforms are defined in an archtecture configuration
 file which are stored by default in scripts/sanity_chk/arches/. These
@@ -120,6 +166,11 @@
     sys.stderr.write("$ZEPHYR_BASE environment variable undefined.\n")
     exit(1)
 ZEPHYR_BASE = os.environ["ZEPHYR_BASE"]
+
+sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/"))
+
+import expr_parser
+
 VERBOSE = 0
 LAST_SANITY = os.path.join(ZEPHYR_BASE, "scripts", "sanity_chk",
                            "last_sanity.csv")
@@ -745,7 +796,7 @@
                        "arch_exclude" : {"type" : "set"},
                        "platform_exclude" : {"type" : "set"},
                        "platform_whitelist" : {"type" : "set"},
-                       "config_whitelist" : {"type" : "set"}}
+                       "filter" : {"type" : "str"}}
 
 
 class SanityConfigParser:
@@ -953,7 +1004,7 @@
     """
     makefile_re = re.compile("\s*KERNEL_TYPE\s*[?=]+\s*(micro|nano)\s*")
 
-    def __init__(self, testcase_root, workdir, name, tc_dict):
+    def __init__(self, testcase_root, workdir, name, tc_dict, inifile):
         """TestCase constructor.
 
         This gets called by TestSuite as it finds and reads testcase.ini files.
@@ -988,7 +1039,7 @@
         self.kernel = tc_dict["kernel"]
         self.platform_exclude = tc_dict["platform_exclude"]
         self.platform_whitelist = tc_dict["platform_whitelist"]
-        self.config_whitelist = tc_dict["config_whitelist"]
+        self.tc_filter = tc_dict["filter"]
         self.timeout = tc_dict["timeout"]
         self.build_only = tc_dict["build_only"]
         self.slow = tc_dict["slow"]
@@ -997,6 +1048,7 @@
         self.name = self.path # for now
         self.defconfig = {}
         self.ktype = None
+        self.inifile = inifile
 
         if self.kernel:
             self.ktype = self.kernel
@@ -1064,7 +1116,7 @@
 
 
 class TestSuite:
-    config_re = re.compile('(CONFIG_[A-Z0-9_]+)[=](.+)$')
+    config_re = re.compile('(CONFIG_[A-Z0-9_]+)[=]\"?([^\"]*)\"?$')
 
     def __init__(self, arch_root, testcase_roots, outdir):
         # Keep track of which test cases we've filtered out and why
@@ -1090,12 +1142,14 @@
                 if "testcase.ini" in filenames:
                     verbose("Found test case in " + dirpath)
                     dirnames[:] = []
-                    cp = SanityConfigParser(os.path.join(dirpath, "testcase.ini"))
+                    ini_path = os.path.join(dirpath, "testcase.ini")
+                    cp = SanityConfigParser(ini_path)
                     workdir = os.path.relpath(dirpath, testcase_root)
 
                     for section in cp.sections():
                         tc_dict = cp.get_section(section, testcase_valid_keys)
-                        tc = TestCase(testcase_root, workdir, section, tc_dict)
+                        tc = TestCase(testcase_root, workdir, section, tc_dict,
+                                      ini_path)
                         self.testcases[tc.name] = tc
 
         debug("Reading architecture configuration files under %s..." % arch_root)
@@ -1189,7 +1243,7 @@
                     if not plat.microkernel_support and tc.ktype == "micro":
                         continue
 
-                    for cw in tc.config_whitelist:
+                    if tc.tc_filter:
                         args = tc.extra_args[:]
                         args.extend(["ARCH=" + plat.arch.name,
                                 "BOARD=" + plat.name, "initconfig"])
@@ -1217,6 +1271,8 @@
                 for line in fp.readlines():
                     m = TestSuite.config_re.match(line)
                     if not m:
+                        if line.strip() and not line.startswith("#"):
+                            sys.stderr.write("Unrecognized line %s\n" % line)
                         continue
                     defconfig[m.group(1)] = m.group(2).strip()
             test.defconfig[plat,ktype] = defconfig
@@ -1271,44 +1327,23 @@
                         discards[instance] = "No microkernel support for platform"
                         continue
 
-                    defconfig = {}
+                    defconfig = {"ARCH" : arch.name, "PLATFORM" : plat.name}
                     for tcase, tdefconfig in tc.defconfig.items():
                         p, k = tcase
                         if k == tc.ktype and p == plat:
-                            defconfig = tdefconfig
+                            defconfig.update(tdefconfig)
                             break
 
-                    config_pass = True
-
-                    # FIXME this is kind of gross clean it up
-                    for cw in tc.config_whitelist:
-                        invert = (cw[0] == "!")
-                        if invert:
-                            cw = cw[1:]
-
-                        if "=" in cw:
-                            k, v = cw.split("=")
-                            testval = k not in defconfig or defconfig[k] != v
-                            if invert:
-                                testval = not testval
-                            if testval:
-                                discards[instance] = "%s%s in platform defconfig" % (
-                                        cw, " not" if not invert else "")
-                                config_pass = False
-                                break
-                        else:
-                            testval = cw not in defconfig
-                            if invert:
-                                testval = not testval
-                            if testval:
-                                discards[instance] = "%s%s set in platform defconfig" % (
-                                        cw, " not" if not invert else "")
-                                config_pass = False
-                                break
-
-                    if not config_pass:
-                        continue
-
+                    if tc.tc_filter:
+                        try:
+                            res = expr_parser.parse(tc.tc_filter, defconfig)
+                        except SyntaxError as se:
+                            sys.stderr.write("Failed processing %s\n" % tc.inifile)
+                            raise se
+                        if not res:
+                            discards[instance] = ("defconfig doesn't satisfy expression '%s'" %
+                                    tc.tc_filter)
+                            continue
 
                     instance_list.append(instance)