[dvsim] Enable coverage collection with Xcelium

- Enabled coverage collection with Xcelium
  - Added Xcelium too opts
  - Made fixes in SimCfg class to add support for it
  - Fixed coverage 'bug' caught by Xceluim in `tl_agent_cov`

Publishing reports to the web server is disabled.

Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/util/dvsim/Deploy.py b/util/dvsim/Deploy.py
index e412c40..d7e2a68 100644
--- a/util/dvsim/Deploy.py
+++ b/util/dvsim/Deploy.py
@@ -17,6 +17,7 @@
 import hjson
 from tabulate import tabulate
 
+from sim_utils import *
 from utils import *
 
 
@@ -170,6 +171,11 @@
             # If renew_odir flag is True - then move it.
             if self.renew_odir: self.odir_limiter(odir=self.odir)
             os.system("mkdir -p " + self.odir)
+            # Dump all env variables for ease of debug.
+            with open(self.odir + "/env_vars", "w") as f:
+                for var in sorted(self.exports.keys()):
+                    f.write("{}={}\n".format(var, self.exports[var]))
+                f.close()
             os.system("ln -s " + self.odir + " " + self.sim_cfg.links['D'] +
                       '/' + self.odir_ln)
             f = open(self.log, "w")
@@ -744,6 +750,9 @@
         if self.sim_cfg.cov_merge_previous:
             self.cov_db_dirs += prev_cov_db_dirs
 
+        # Append cov_db_dirs to the list of exports.
+        self.exports["cov_db_dirs"] = "\"{}\"".format(self.cov_db_dirs)
+
         # Call base class __post_init__ to do checks and substitutions
         super().__post_init__()
 
@@ -772,7 +781,7 @@
         self.mandatory_misc_attrs = {
             "cov_report_dir": False,
             "cov_merge_db_dir": False,
-            "cov_report_dashboard": False
+            "cov_report_txt": False
         }
 
         # Initialize
@@ -791,47 +800,21 @@
         super().get_status()
         # Once passed, extract the cov results summary from the dashboard.
         if self.status == "P":
-            try:
-                with open(self.cov_report_dashboard, 'r') as f:
-                    for line in f:
-                        match = re.match("total coverage summary", line,
-                                         re.IGNORECASE)
-                        if match:
-                            results = []
-                            # Metrics on the next line.
-                            line = f.readline().strip()
-                            results.append(line.split())
-                            # Values on the next.
-                            line = f.readline().strip()
-                            # Pretty up the values - add % sign for ease of post
-                            # processing.
-                            values = []
-                            for val in line.split():
-                                val += " %"
-                                values.append(val)
-                            # first row is coverage total
-                            self.cov_total = values[0]
-                            results.append(values)
-                            colalign = (("center", ) * len(values))
-                            self.cov_results = tabulate(results,
-                                                        headers="firstrow",
-                                                        tablefmt="pipe",
-                                                        colalign=colalign)
-                            break
+            results, self.cov_total, ex_msg = get_cov_summary_table(
+                self.cov_report_txt, self.sim_cfg.tool)
 
-            except Exception as e:
-                ex_msg = "Failed to parse \"{}\":\n{}".format(
-                    self.cov_report_dashboard, str(e))
+            if not ex_msg:
+                # Succeeded in obtaining the coverage data.
+                colalign = (("center", ) * len(results[0]))
+                self.cov_results = tabulate(results,
+                                            headers="firstrow",
+                                            tablefmt="pipe",
+                                            colalign=colalign)
+            else:
                 self.fail_msg += ex_msg
                 log.error(ex_msg)
                 self.status = "F"
 
-            if self.cov_results == "":
-                nf_msg = "Coverage summary not found in the reports dashboard!"
-                self.fail_msg += nf_msg
-                log.error(nf_msg)
-                self.status = "F"
-
         if self.status == "P":
             # Delete the cov report - not needed.
             os.system("rm -rf " + self.log)
@@ -851,6 +834,9 @@
         self.fail_patterns = []
 
         self.mandatory_cmd_attrs = {
+            # tool srcs
+            "tool_srcs": False,
+            "tool_srcs_dir": False,
             "cov_analyze_cmd": False,
             "cov_analyze_opts": False
         }
diff --git a/util/dvsim/SimCfg.py b/util/dvsim/SimCfg.py
index 91562ba..55157c2 100644
--- a/util/dvsim/SimCfg.py
+++ b/util/dvsim/SimCfg.py
@@ -53,6 +53,9 @@
         self.dry_run = args.dry_run
         self.map_full_testplan = args.map_full_testplan
 
+        # Disable cov if --build-only is passed.
+        if self.build_only: self.cov = False
+
         # Set default sim modes for unpacking
         if self.waves is True: self.en_build_modes.append("waves")
         if self.cov is True: self.en_build_modes.append("cov")
@@ -145,12 +148,6 @@
 
             # Create objects from raw dicts - build_modes, sim_modes, run_modes,
             # tests and regressions, only if not a master cfg obj
-            # TODO: hack to prevent coverage collection if tool != vcs
-            if self.cov and self.tool != "vcs":
-                self.cov = False
-                log.warning(
-                    "Coverage collection with tool \"%s\" is not supported yet",
-                    self.tool)
             self._create_objects()
 
         # Post init checks
@@ -405,14 +402,7 @@
         analyze the coverage.
         '''
         cov_analyze_deploy = CovAnalyze(self)
-        try:
-            proc = subprocess.Popen(args=cov_analyze_deploy.cmd,
-                                    shell=True,
-                                    close_fds=True)
-        except Exception as e:
-            log.fatal("Failed to run coverage analysis cmd:\n\"%s\"\n%s",
-                      cov_analyze_deploy.cmd, e)
-            sys.exit(1)
+        self.deploy = [cov_analyze_deploy]
 
     def cov_analyze(self):
         '''Public facing API for analyzing coverage.
@@ -500,12 +490,17 @@
             if self.cov:
                 if self.cov_report_deploy.status == "P":
                     results_str += "\n## Coverage Results\n"
-                    results_str += "\n### [Coverage Dashboard]"
-                    results_str += "(cov_report/dashboard.html)\n\n"
+                    # Link the dashboard page using "cov_report_page" value.
+                    # TODO: hack to only link VCS generated results.
+                    if self.tool == "vcs" and hasattr(self, "cov_report_page"):
+                        results_str += "\n### [Coverage Dashboard]"
+                        results_str += "({})\n\n".format(
+                            getattr(self, "cov_report_page"))
                     results_str += self.cov_report_deploy.cov_results
                     self.results_summary[
                         "Coverage"] = self.cov_report_deploy.cov_total
-                self.results_summary["Coverage"] = "--"
+                else:
+                    self.results_summary["Coverage"] = "--"
 
             # append link of detail result to block name
             self.results_summary["Name"] = self._get_results_page_link(
@@ -552,6 +547,8 @@
         super()._publish_results()
 
         if self.cov:
+            # TODO: hack to only allow VCS coverage data to be uploaded.
+            if self.tool != "vcs": return
             results_server_dir_url = self.results_server_dir.replace(
                 self.results_server_prefix, self.results_server_url_prefix)
 
diff --git a/util/dvsim/dvsim.py b/util/dvsim/dvsim.py
index b832fc8..93d8082 100755
--- a/util/dvsim/dvsim.py
+++ b/util/dvsim/dvsim.py
@@ -491,6 +491,7 @@
     # tool.
     if args.cov_analyze:
         cfg.cov_analyze()
+        cfg.deploy_objects()
         sys.exit(0)
 
     # Purge the scratch path if --purge option is set.
diff --git a/util/dvsim/sim_utils.py b/util/dvsim/sim_utils.py
new file mode 100644
index 0000000..3a50536
--- /dev/null
+++ b/util/dvsim/sim_utils.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+"""
+This script provides common DV simulation specific utilities.
+"""
+
+import re
+from collections import OrderedDict
+
+
+# Capture the summary results as a list of lists.
+# The text coverage report is passed as input to the function, in addition to
+# the tool used. The tool returns a 2D list if the coverage report file was read
+# and the coverage was extracted successfully. It returns a tuple of:
+#   List of metrics and values
+#   Final coverage total
+#   Error message, if failed
+def get_cov_summary_table(cov_report_txt, tool):
+    try:
+        with open(cov_report_txt, 'r') as f:
+            if tool == 'xcelium':
+                return xcelium_cov_summary_table(f)
+            if tool == 'vcs':
+                return vcs_cov_summary_table(f)
+
+            err_msg = "Unsupported tool for cov extraction: {}".format(tool)
+            return None, None, err_msg
+
+    except Exception as e:
+        err_msg = "Exception occurred: {}".format(str(e))
+        return None, None, err_msg
+
+
+# Same desc as above, but specific to Xcelium and takes an opened input stream.
+def xcelium_cov_summary_table(buf):
+    for line in buf:
+        if "name" in line:
+            # Strip the line and remove the unwanted "* Covered" string.
+            metrics = line.strip().replace("* Covered", "").split()
+            # Change first item to 'Score'.
+            metrics[0] = 'Score'
+
+            # metric.
+            items = OrderedDict()
+            for metric in metrics:
+                items[metric] = {}
+                items[metric]['covered'] = 0
+                items[metric]['total'] = 0
+            # Next line is a separator.
+            line = buf.readline()
+            # Subsequent lines are coverage items to be aggregated.
+            for line in buf:
+                line = re.sub(r"%\s+\(", "%(", line)
+                values = line.strip().split()
+                for i, value in enumerate(values):
+                    value = value.strip()
+                    m = re.search(r"\((\d+)/(\d+)\)", value)
+                    if m:
+                        items[metrics[i]]['covered'] += int(m.group(1))
+                        items[metrics[i]]['total'] += int(m.group(2))
+                        items['Score']['covered'] += int(m.group(1))
+                        items['Score']['total'] += int(m.group(2))
+            # Capture the percentages and the aggregate.
+            values = []
+            cov_total = None
+            for metric in items.keys():
+                if items[metric]['total'] == 0: values.append("-- %")
+                else:
+                    value = items[metric]['covered'] / items[metric][
+                        'total'] * 100
+                    value = "{0:.2f} %".format(round(value, 2))
+                    values.append(value)
+                    if metric == 'Score': cov_total = value
+            return [metrics, values], cov_total, None
+
+    # If we reached here, then we were unable to extract the coverage.
+    err_msg = "ParseError: coverage data not found!"
+    return None, None, err_msg
+
+
+# Same desc as above, but specific to VCS and takes an opened input stream.
+def vcs_cov_summary_table(buf):
+    for line in buf:
+        match = re.match("total coverage summary", line, re.IGNORECASE)
+        if match:
+            # Metrics on the next line.
+            line = buf.readline().strip()
+            metrics = line.split()
+            # Values on the next.
+            line = buf.readline().strip()
+            # Pretty up the values - add % sign for ease of post
+            # processing.
+            values = []
+            for val in line.split():
+                val += " %"
+                values.append(val)
+            # first row is coverage total
+            cov_total = values[0]
+            return [metrics, values], cov_total, None
+
+    # If we reached here, then we were unable to extract the coverage.
+    err_msg = "ParseError: coverage data not found!"
+    return None, None, err_msg