[dv regr tool] Coverage collection and reporting

- This PR enables coverage collection as a part of the regression run
when --cov switch is passed
- If there are multiple builds as a part of the same DUT, it merges the
coverage across them
- It also merges coverage from previous regressions if the
--cov-merge-previous switch is passed
- Finally, it extracts the high level summary coverage from the VCS
coverage dashboard and prints it as a part of the regression report

Another major update in this PR is - all percentages indicated in a
report table indicated with a '%' sign is automatically colored in the
html report as a heat map, from red for low percentages to green
approaching 100%. This is enabled for regression results as well as
coverage results.

Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/util/dvsim/utils.py b/util/dvsim/utils.py
index 0d124dc..0e95a41 100644
--- a/util/dvsim/utils.py
+++ b/util/dvsim/utils.py
@@ -79,8 +79,9 @@
         hjson_cfg_dict = hjson.loads(text, use_decimal=True)
         f.close()
     except Exception as e:
-        log.fatal("Failed to parse \"%s\" possibly due to bad path or syntax error.\n%s",
-                  hjson_file, e)
+        log.fatal(
+            "Failed to parse \"%s\" possibly due to bad path or syntax error.\n%s",
+            hjson_file, e)
         sys.exit(1)
     return hjson_cfg_dict
 
@@ -95,15 +96,19 @@
 
     if "{eval_cmd}" in var:
         idx = var.find("{eval_cmd}") + 11
-        var = subst_wildcards(var[idx:], mdict, ignored_wildcards)
-        var = run_cmd(var)
+        subst_var = subst_wildcards(var[idx:], mdict, ignored_wildcards)
+        # If var has wildcards that were ignored, then skip running the command
+        # for now, assume that it will be handled later.
+        match = re.findall(r"{([A-Za-z0-9\_]+)}", subst_var)
+        if len(match) == 0:
+            var = run_cmd(subst_var)
     else:
         match = re.findall(r"{([A-Za-z0-9\_]+)}", var)
         if len(match) > 0:
             subst_list = {}
             for item in match:
                 if item not in ignored_wildcards:
-                    log.debug("Found wildcard in \"%s\": \"%s\"", var, item)
+                    log.debug("Found wildcard \"%s\" in \"%s\"", item, var)
                     found = subst(item, mdict)
                     if found is not None:
                         if type(found) is list:
@@ -190,4 +195,68 @@
     html_text += "</div>\n"
     html_text += "</body>\n"
     html_text += "</html>\n"
+    html_text = htmc_color_pc_cells(html_text)
     return html_text
+
+
+def htmc_color_pc_cells(text):
+    '''This function finds cells in a html table that contains a % sign. It then
+    uses the number in front if the % sign to color the cell based on the value
+    from a shade from red to green. These color styles are encoded in ./style.css
+    which is assumed to be accessible by the final webpage.
+    '''
+
+    # Replace <td> with <td class="color-class"> based on the fp
+    # value. "color-classes" are listed in ./style.css as follows: "cna"
+    # for NA value, "c0" to "c10" for fp value falling between 0.00-9.99,
+    # 10.00-19.99 ... 90.00-99.99, 100.0 respetively.
+    def color_cell(cell, cclass):
+        op = cell.replace("<td", "<td class=\"" + cclass + "\"")
+        # Remove '%' sign.
+        op = re.sub(r"\s*%\s*", "", op)
+        return op
+
+    # List of 'not applicable' identifiers.
+    na_list = ['--', 'NA', 'N.A.', 'N.A', 'na', 'n.a.', 'n.a']
+    na_list_patterns = '|'.join(na_list)
+
+    # List of floating point patterns: '0', '0.0' & '.0'
+    fp_patterns = "\d+|\d+\.\d+|\.\d+"
+
+    patterns = fp_patterns + '|' + na_list_patterns
+    match = re.findall(r"(<td.*>\s*(" + patterns + ")\s+%\s*</td>)", text)
+    if len(match) > 0:
+        subst_list = {}
+        fp_nums = []
+        for item in match:
+            # item is a tuple - first is the full string indicating the table
+            # cell which we want to replace, second is the floating point value.
+            cell = item[0]
+            fp_num = item[1]
+            # Skip if fp_num is already processed.
+            if fp_num in fp_nums: continue
+            fp_nums.append(fp_num)
+            if fp_num in na_list: subst = color_cell(cell, "cna")
+            else:
+                # Item is a fp num.
+                try:
+                    fp = float(fp_num)
+                except ValueError:
+                    log.error("Percentage item \"%s\" in cell \"%s\" is not an " + \
+                              "integer or a floating point number", fp_num, cell)
+                    continue
+                if fp >= 0.0 and fp < 10.0: subst = color_cell(cell, "c0")
+                elif fp >= 10.0 and fp < 20.0: subst = color_cell(cell, "c1")
+                elif fp >= 20.0 and fp < 30.0: subst = color_cell(cell, "c2")
+                elif fp >= 30.0 and fp < 40.0: subst = color_cell(cell, "c3")
+                elif fp >= 40.0 and fp < 50.0: subst = color_cell(cell, "c4")
+                elif fp >= 50.0 and fp < 60.0: subst = color_cell(cell, "c5")
+                elif fp >= 60.0 and fp < 70.0: subst = color_cell(cell, "c6")
+                elif fp >= 70.0 and fp < 80.0: subst = color_cell(cell, "c7")
+                elif fp >= 80.0 and fp < 90.0: subst = color_cell(cell, "c8")
+                elif fp >= 90.0 and fp < 100.0: subst = color_cell(cell, "c9")
+                elif fp >= 100.0: subst = color_cell(cell, "c10")
+            subst_list[cell] = subst
+        for item in subst_list:
+            text = text.replace(item, subst_list[item])
+    return text