sanitycheck: parse test cases from source files

This parses the tests that run within a test project/application from
the source code and gives us a view of what was run, skipped and what
was blocked due to early termination of the test.

Signed-off-by: Anas Nashif <anas.nashif@intel.com>
diff --git a/scripts/sanitycheck b/scripts/sanitycheck
index 5f49356..79714fd 100755
--- a/scripts/sanitycheck
+++ b/scripts/sanitycheck
@@ -158,6 +158,8 @@
 Most everyday users will run with no arguments.
 """
 
+import contextlib
+import mmap
 import argparse
 import os
 import sys
@@ -1362,6 +1364,8 @@
             from the testcase.yaml file
         """
         self.code_location = os.path.join(testcase_root, workdir)
+        self.id = name
+        self.cases = []
         self.type = tc_dict["type"]
         self.tags = tc_dict["tags"]
         self.extra_args = tc_dict["extra_args"]
@@ -1389,10 +1393,77 @@
             testcase_root).replace(os.path.realpath(ZEPHYR_BASE) + "/", ''),
             workdir, name))
 
+
         self.name = os.path.join(self.path)
         self.defconfig = {}
         self.yamlfile = yamlfile
 
+    def scan_file(self, inf_name):
+        include_regex = re.compile(
+            br"#include\s*<ztest\.h>",
+            re.MULTILINE)
+        suite_regex = re.compile(
+            br"^\s*ztest_test_suite\(\s*(?P<suite_name>[a-zA-Z0-9_]+)[\s,]*$",
+            re.MULTILINE)
+        stc_regex = re.compile(
+            br"^\s*ztest_(user_)?unit_test\((test_)?(?P<stc_name>[a-zA-Z0-9_]+)\)[\s,;\)]*$",
+            re.MULTILINE)
+        suite_run_regex = re.compile(
+            br"^\s*ztest_run_test_suite\((?P<suite_name>[a-zA-Z0-9_]+)\)",
+            re.MULTILINE)
+        achtung_regex = re.compile(
+            br"(#ifdef|#endif)",
+            re.MULTILINE)
+        warnings = None
+
+        with open(inf_name) as inf:
+            with contextlib.closing(mmap.mmap(inf.fileno(), 0, mmap.MAP_PRIVATE,
+                                              mmap.PROT_READ, 0)) as main_c:
+                #if not include_regex.search(main_c):
+                #    return None, None #"skipped, not using ztest.h"
+
+                suite_regex_match = suite_regex.search(main_c)
+                if not suite_regex_match:
+                    # can't find ztest_test_suite, maybe a client, because
+                    # it includes ztest.h
+                    return None, None
+
+                suite_run_match = suite_run_regex.search(main_c)
+                if not suite_run_match:
+                    raise ValueError("can't find ztest_run_test_suite")
+
+                achtung_matches = re.findall(
+                    achtung_regex,
+                    main_c[suite_regex_match.end():suite_run_match.start()])
+                if achtung_matches:
+                    warnings = "found invalid %s in ztest_test_suite()" \
+                               % ", ".join(set(achtung_matches))
+                matches = re.findall(
+                    stc_regex,
+                    main_c[suite_regex_match.end():suite_run_match.start()])
+                return matches, warnings
+
+    def scan_path(self, path):
+        subcases = []
+        for filename in glob.glob(os.path.join(path, "src", "*.c")):
+            try:
+                _subcases, warnings = self.scan_file(filename)
+                if warnings:
+                    warning("%s: %s", filename, warnings)
+                if _subcases:
+                    subcases += _subcases
+            except ValueError as e:
+                error("%s: can't find: %s", filename, e)
+        return subcases
+
+
+    def parse_subcases(self):
+        results = self.scan_path(self.code_location)
+        for sub in results:
+            name = "{}.{}".format(self.id, sub[2].decode())
+            self.cases.append(name)
+
+
     def __str__(self):
         return self.name
 
@@ -1505,6 +1576,7 @@
                         tc_dict = parsed_data.get_test(name, testcase_valid_keys)
                         tc = TestCase(testcase_root, workdir, name, tc_dict,
                                       yaml_path)
+                        tc.parse_subcases()
 
                         self.testcases[tc.name] = tc