sanitycheck: add harness classes

Add 2 classes, one to handle the current TestCase scenario, and one more
for handling generic Console with regex matching.

Signed-off-by: Anas Nashif <anas.nashif@intel.com>
diff --git a/scripts/sanitycheck b/scripts/sanitycheck
index b3741a1..96e7bbe 100755
--- a/scripts/sanitycheck
+++ b/scripts/sanitycheck
@@ -217,7 +217,6 @@
     COLOR_GREEN = ""
     COLOR_YELLOW = ""
 
-
 class SanityCheckException(Exception):
     pass
 
@@ -275,12 +274,20 @@
     if VERBOSE >= 2:
         info(what)
 
+class HarnessImporter:
+
+    def __init__(self, name):
+        sys.path.insert(0, os.path.join(ZEPHYR_BASE, "scripts/sanity_chk"))
+        module = __import__("harness")
+        if name:
+            my_class = getattr(module, name)
+        else:
+            my_class = getattr(module, "Test")
+
+        self.instance = my_class()
 
 class Handler:
-    RUN_PASSED = "PROJECT EXECUTION SUCCESSFUL"
-    RUN_FAILED = "PROJECT EXECUTION FAILED"
-
-    def __init__(self, name, outdir, log_fn, timeout, unit=False):
+    def __init__(self, instance):
         """Constructor
 
         @param name Arbitrary name of the created thread
@@ -296,7 +303,6 @@
         self.metrics["handler_time"] = 0
         self.metrics["ram_size"] = 0
         self.metrics["rom_size"] = 0
-        self.unit = unit
 
     def set_state(self, state, metrics):
         self.lock.acquire()
@@ -311,7 +317,7 @@
         return ret
 
 class NativeHandler(Handler):
-    def __init__(self, name, sourcedir, outdir, run_log, valgrind_log, timeout):
+    def __init__(self, instance):
         """Constructor
 
         @param name Arbitrary name of the created thread
@@ -321,58 +327,68 @@
         @param timeout Kill the QEMU process if it doesn't finish up within
             the given number of seconds
         """
-        super().__init__(name, outdir, run_log, timeout, True)
+        super().__init__(instance)
 
-        self.timeout = timeout
-        self.sourcedir = sourcedir
-        self.outdir = outdir
-        self.run_log = run_log
+        self.instance = instance
+        self.timeout = instance.test.timeout
+        self.sourcedir = instance.test.code_location
+        self.outdir = instance.outdir
+        self.run_log = os.path.join(self.outdir, "run.log")
+        self.valgrind_log = os.path.join(self.outdir, "valgrind.log")
         self.valgrind = False
-        self.valgrind_log = valgrind_log
         self.returncode = 0
         self.set_state("running", {})
 
+    def output_reader(self, proc, harness):
+        for line in iter(proc.stdout.readline, b''):
+            verbose("NATIVE: {0}".format(line.decode('utf-8').rstrip()))
+            harness.handle(line.decode('utf-8').rstrip())
+            if harness.state:
+                proc.terminate()
+                break
+
     def handle(self):
         out_state = "failed"
 
-        with open(self.run_log, "wt") as rl, open(self.valgrind_log, "wt") as vl:
-            try:
-                binary = os.path.join(self.outdir, "zephyr", "zephyr.exe")
-                command = [binary]
-                if shutil.which("valgrind") and self.valgrind:
-                    command = ["valgrind", "--error-exitcode=2",
-                               "--leak-check=full"] + command
-                returncode = subprocess.call(command, timeout=self.timeout,
-                                             stdout=rl, stderr=vl)
-                self.returncode = returncode
-                if returncode != 0:
-                    if self.returncode == 1:
-                        out_state = "failed"
-                    else:
-                        out_state = "failed valgrind"
+        harness_name = self.instance.test.harness.capitalize()
+        harness_import = HarnessImporter(harness_name)
+        harness = harness_import.instance
+        harness.configure(self.instance)
 
-            except subprocess.TimeoutExpired:
+        binary = os.path.join(self.outdir, "zephyr", "zephyr.exe")
+        command = [binary]
+        if shutil.which("valgrind") and self.valgrind:
+            command = ["valgrind", "--error-exitcode=2",
+                       "--leak-check=full"] + command
+
+        with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
+            t = threading.Thread(target=self.output_reader, args=(proc, harness, ))
+            t.start()
+            t.join(self.timeout)
+            if t.is_alive():
+                proc.terminate()
                 out_state = "timeout"
-                self.returncode = 1
+                t.join()
 
-        with open(self.run_log, "r") as rl:
-            for line in rl.readlines():
-                line = line.strip()
-                if self.RUN_PASSED in line:
-                    out_state = "passed"
-                    break
-
-                if self.RUN_FAILED in line:
+            proc.wait()
+            self.returncode = proc.returncode
+            if proc.returncode != 0:
+                if self.returncode == 1:
                     out_state = "failed"
-                    break
+                else:
+                    out_state = "failed valgrind"
 
-        returncode = subprocess.call(["GCOV_PREFIX=" + self.outdir, "gcov", self.sourcedir, "-s", self.outdir], shell=True)
+        #print(" ".join(["GCOV_PREFIX=" + self.outdir, "gcov", self.sourcedir, "-b", "-s", self.outdir]))
+        returncode = subprocess.call(["GCOV_PREFIX=" + self.outdir, "gcov", self.sourcedir, "-b", "-s", self.outdir], shell=True)
 
-        self.set_state(out_state, {})
+        if harness.state:
+            self.set_state(harness.state, {})
+        else:
+            self.set_state(out_state, {})
+
 
 class UnitHandler(Handler):
-    def __init__(self, name, sourcedir, outdir,
-                 run_log, valgrind_log, timeout):
+    def __init__(self, instance):
         """Constructor
 
         @param name Arbitrary name of the created thread
@@ -382,13 +398,13 @@
         @param timeout Kill the QEMU process if it doesn't finish up within
             the given number of seconds
         """
-        super().__init__(name, outdir, run_log, timeout, True)
+        super().__init__(instance)
 
-        self.timeout = timeout
-        self.sourcedir = sourcedir
-        self.outdir = outdir
-        self.run_log = run_log
-        self.valgrind_log = valgrind_log
+        self.timeout = instance.test.timeout
+        self.sourcedir = instance.test.code_location
+        self.outdir = instance.outdir
+        self.run_log = os.path.join(self.outdir, "run.log")
+        self.valgrind_log = os.path.join(self.outdir, "valgrind.log")
         self.returncode = 0
         self.set_state("running", {})
 
@@ -432,7 +448,7 @@
     """
 
     @staticmethod
-    def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results):
+    def _thread(handler, timeout, outdir, logfile, fifo_fn, pid_fn, results, harness):
         fifo_in = fifo_fn + ".in"
         fifo_out = fifo_fn + ".out"
 
@@ -486,12 +502,9 @@
             line = line.strip()
             verbose("QEMU: %s" % line)
 
-            if line == handler.RUN_PASSED:
-                out_state = "passed"
-                break
-
-            if line == handler.RUN_FAILED:
-                out_state = "failed"
+            harness.handle(line)
+            if harness.state:
+                out_state = harness.state
                 break
 
             # TODO: Add support for getting numerical performance data
@@ -519,7 +532,7 @@
         os.unlink(fifo_in)
         os.unlink(fifo_out)
 
-    def __init__(self, name, outdir, log_fn, timeout):
+    def __init__(self, instance):
         """Constructor
 
         @param name Arbitrary name of the created thread
@@ -529,22 +542,34 @@
         @param timeout Kill the QEMU process if it doesn't finish up within
             the given number of seconds
         """
-        super().__init__(name, outdir, log_fn, timeout)
+
+
+        super().__init__(instance)
+        outdir = instance.outdir
+        timeout = instance.test.timeout
+        name = instance.name
+        run_log = os.path.join(outdir, "run.log")
+        qemu_log = os.path.join(outdir, "qemu.log")
+
         self.results = {}
 
         # We pass this to QEMU which looks for fifos with .in and .out
         # suffixes.
-        self.fifo_fn = os.path.join(outdir, "qemu-fifo")
+        self.fifo_fn = os.path.join(instance.outdir, "qemu-fifo")
 
-        self.pid_fn = os.path.join(outdir, "qemu.pid")
+        self.pid_fn = os.path.join(instance.outdir, "qemu.pid")
         if os.path.exists(self.pid_fn):
             os.unlink(self.pid_fn)
 
-        self.log_fn = log_fn
+        self.log_fn = qemu_log
+
+        harness_import = HarnessImporter(instance.test.harness.capitalize())
+        harness = harness_import.instance
+        harness.configure(instance)
         self.thread = threading.Thread(name=name, target=QEMUHandler._thread,
                                        args=(self, timeout, outdir,
                                              self.log_fn, self.fifo_fn,
-                                             self.pid_fn, self.results))
+                                             self.pid_fn, self.results, harness))
         self.thread.daemon = True
         verbose("Spawning QEMU process for %s" % name)
         self.thread.start()
@@ -843,6 +868,11 @@
         if not os.path.exists(outdir):
             os.makedirs(outdir)
 
+    def add_instance_build_goal(self, instance, args, buildlog, make_args=""):
+
+        self.add_build_goal(instance.name, instance.test.code_location,
+                instance.outdir, args, buildlog, make_args)
+
     def add_build_goal(self, name, directory, outdir,
                        args, buildlog, make_args=""):
         """Add a goal to invoke a Kbuild session
@@ -878,7 +908,7 @@
             None,
             None)
 
-    def add_qemu_goal(self, name, directory, outdir, args, timeout=30):
+    def add_qemu_goal(self, instance, args):
         """Add a goal to build a Zephyr project and then run it under QEMU
 
         The generated make goal invokes Make twice, the first time it will
@@ -898,12 +928,16 @@
             to run before automatically killing it. Default is 30 seconds.
         """
 
-        self._add_goal(outdir)
+        name = instance.name
+        directory = instance.test.code_location
+        outdir = instance.outdir
+
         build_logfile = os.path.join(outdir, "build.log")
         run_logfile = os.path.join(outdir, "run.log")
         qemu_logfile = os.path.join(outdir, "qemu.log")
+        self._add_goal(outdir)
 
-        qemu_handler = QEMUHandler(name, outdir, qemu_logfile, timeout)
+        qemu_handler = QEMUHandler(instance)
         args.append("QEMU_PIPE=%s" % qemu_handler.get_fifo())
         text = (self._get_rule_header(name) +
                 self._get_sub_make(name, "building", directory,
@@ -915,8 +949,12 @@
         self.goals[name] = MakeGoal(name, text, qemu_handler, self.logfile, build_logfile,
                                     run_logfile, qemu_logfile)
 
-    def add_unit_goal(self, name, directory, outdir,
-                      args, timeout=30, coverage=False):
+    def add_unit_goal(self, instance, args, timeout=30, coverage=False):
+        outdir = instance.outdir
+        timeout = instance.test.timeout
+        name = instance.name
+        directory = instance.test.code_location
+
         self._add_goal(outdir)
         build_logfile = os.path.join(outdir, "build.log")
         run_logfile = os.path.join(outdir, "run.log")
@@ -929,11 +967,17 @@
                 self._get_sub_make(name, "building", directory,
                                    outdir, build_logfile, args) +
                 self._get_rule_footer(name))
-        unit_handler = UnitHandler(name, directory, outdir, run_logfile, valgrind_logfile, timeout)
+        unit_handler = UnitHandler(instance)
         self.goals[name] = MakeGoal(name, text, unit_handler, self.logfile, build_logfile,
                                     run_logfile, valgrind_logfile)
 
-    def add_native_goal(self, name, directory, outdir, args, timeout=30, coverage=False):
+    def add_native_goal(self, instance, args, coverage=False):
+
+        outdir = instance.outdir
+        timeout = instance.test.timeout
+        name = instance.name
+        directory = instance.test.code_location
+
         self._add_goal(outdir)
         build_logfile = os.path.join(outdir, "build.log")
         run_logfile = os.path.join(outdir, "run.log")
@@ -944,7 +988,7 @@
                 self._get_sub_make(name, "building", directory,
                                    outdir, build_logfile, args) +
                 self._get_rule_footer(name))
-        native_handler = NativeHandler(name, directory, outdir, run_logfile, valgrind_logfile, timeout)
+        native_handler = NativeHandler(instance)
         self.goals[name] = MakeGoal(name, text, native_handler, self.logfile, build_logfile,
                                     run_logfile, valgrind_logfile)
 
@@ -964,17 +1008,13 @@
         args.extend(extra_args)
         if (ti.platform.qemu_support and (not ti.build_only) and
                 (not build_only) and (enable_slow or not ti.test.slow)):
-            self.add_qemu_goal(ti.name, ti.test.code_location, ti.outdir,
-                               args, ti.test.timeout)
+            self.add_qemu_goal(ti, args)
         elif ti.test.type == "unit":
-            self.add_unit_goal(ti.name, ti.test.code_location, ti.outdir,
-                               args, ti.test.timeout, coverage)
+            self.add_unit_goal(ti, args, coverage)
         elif ti.platform.type == "native" and (not ti.build_only) and (not build_only):
-            self.add_native_goal(ti.name, ti.test.code_location, ti.outdir,
-                               args, ti.test.timeout, coverage)
+            self.add_native_goal(ti, args, coverage)
         else:
-            self.add_build_goal(ti.name, ti.test.code_location, ti.outdir,
-                                args, "build.log")
+            self.add_instance_build_goal(ti, args, "build.log")
 
     def execute(self, callback_fn=None, context=None):
         """Execute all the registered build goals
@@ -1029,8 +1069,7 @@
 
                     if state == "finished":
                         if goal.handler:
-                            if goal.handler.unit:
-                                # We can't run unit tests with Make
+                            if hasattr(goal.handler, "handle"):
                                 goal.handler.handle()
                                 if goal.handler.returncode == 2:
                                     goal.handler_log = goal.handler.valgrind_log
@@ -1087,7 +1126,8 @@
                        "toolchain_exclude": {"type": "set"},
                        "toolchain_whitelist": {"type": "set"},
                        "filter": {"type": "str"},
-                       "harness": {"type": "str"}
+                       "harness": {"type": "str"},
+                       "harness_config": {"type": "map"}
                        }
 
 
@@ -1140,6 +1180,8 @@
             else:
                 return set(vs)
 
+        elif typestr.startswith("map"):
+            return value
         else:
             raise ConfigurationError(
                 self.filename, "unknown type '%s'" % value)
@@ -1319,6 +1361,7 @@
         self.tc_filter = tc_dict["filter"]
         self.timeout = tc_dict["timeout"]
         self.harness = tc_dict["harness"]
+        self.harness_config = tc_dict["harness_config"]
         self.build_only = tc_dict["build_only"]
         self.build_on_all = tc_dict["build_on_all"]
         self.slow = tc_dict["slow"]
@@ -1352,9 +1395,7 @@
         self.platform = platform
         self.name = os.path.join(platform.name, test.name)
         self.outdir = os.path.join(base_outdir, platform.name, test.path)
-        # TODO: Support harness in sanitycheck, now we do not run on anything
-        # that requires a harness
-        self.build_only = build_only or test.build_only or (test.harness != '')
+        self.build_only = build_only or test.build_only or (test.harness and test.harness != 'console')
 
     def create_overlay(self):
         if len(self.test.extra_configs) > 0:
@@ -1629,11 +1670,10 @@
                             "/")[-1]] = os.path.join(o, "zephyr", ".config")
                         goal = "_".join([plat.name, "_".join(
                             tc.name.split("/")), "config-sanitycheck"])
-                        mg.add_build_goal(
-                            goal, os.path.join(
-                                ZEPHYR_BASE, tc.code_location),
-                            o, args, "config-sanitycheck.log",
-                            make_args="config-sanitycheck")
+                        mg.add_build_goal(goal,
+                                os.path.join(ZEPHYR_BASE, tc.code_location),
+                                o, args,
+                                "config-sanitycheck.log", make_args="config-sanitycheck")
 
         info("Building testcase defconfigs...")
         results = mg.execute(defconfig_cb)
@@ -2289,7 +2329,7 @@
                 ["lcov", "--remove", coveragefile, i, "--output-file",
                  coveragefile],
                 stdout=coveragelog)
-        subprocess.call(["genhtml", "-output-directory",
+        subprocess.call(["genhtml", "--legend", "-output-directory",
                          os.path.join(outdir, "coverage"),
                          coveragefile, ztestfile], stdout=coveragelog)