[dvsim] Minor cleanup of job_runtime updates

This commit makes the following minor cleanups on the
job_runtime / simulated_time enhancements:
- Move JobTime class to its own file from Testplan
  - This is fine - Testplan does not actually need to know
    the type of job_runtime and simulated_time in the Results
    class. It can be of any numerical type and it will still
    work fine.
  - Associated changes in other sources.
- JobTime class enhancements
  - Make time and unit class members private - add set() and
    get() methods to set / retrieve them
  - Add _normalize() method to normalize the time, unit
    immediately after they are set.
  - Add `__gt__` method to enable obj1 > obj2 comparison
    (done in SimResults.py)
- Cleanup lint errors (run yapf, isort, flake8, mypy)
- Use more robust patterns when matching and extracting
  job_runtime and simulated time in sim_utils.py
  - VCS does not invoke $finish on NOA errors - fix this usecase.
  - Remove SyntaxError exception since it is not needed.
- Rename `Deploy::extract_runtimes()` to a more generic sounding
  `Deploy::extract_info_from_log()`
- Move (Passing, Total, Pass Rate) columns to the end.
- Use dvsim computed job runtime if the extraction of tool provided
  runtime fails.
- Bucketize killed jobs as well.
  - Add Launcher::fail_msg class attribute for jobs that are killed
    when their dependent jobs fail.
- Other general coding style / formatting / comment / docstring
  improvements.

Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/util/dvsim/sim_utils.py b/util/dvsim/sim_utils.py
index 8239b9f..66aa0c7 100644
--- a/util/dvsim/sim_utils.py
+++ b/util/dvsim/sim_utils.py
@@ -105,9 +105,10 @@
 def get_job_runtime(log_text: List, tool: str) -> Tuple[float, str]:
     """Returns the job runtime (wall clock time) along with its units.
 
-    EDA tools indicate how long the job ran in terms of CPU time in the log file.
-    This method invokes the tool specific method which parses the log text and
-    returns the runtime as a floating point value followed by its units as a tuple.
+    EDA tools indicate how long the job ran in terms of CPU time in the log
+    file. This method invokes the tool specific method which parses the log
+    text and returns the runtime as a floating point value followed by its
+    units as a tuple.
 
     `log_text` is the job's log file contents as a list of lines.
     `tool` is the EDA tool used to run the job.
@@ -119,116 +120,104 @@
     elif tool == 'vcs':
         return vcs_job_runtime(log_text)
     else:
-        raise NotImplementedError(f"{tool} is unsupported for job runtime extraction.")
+        raise NotImplementedError(f"{tool} is unsupported for job runtime "
+                                  "extraction.")
 
 
 def vcs_job_runtime(log_text: List) -> Tuple[float, str]:
-    """Capture and return the job_runtime along with its units from VCS simulator.
+    """Returns the VCS job runtime (wall clock time) along with its units.
 
-    Example of VCS job_runtime: 'CPU Time:      1.210 seconds;'
-    Raises SyntaxError exception if we find the specific line but fail to parse the
-    actual number with unit.
+    Search pattern example:
+    CPU time: 22.170 seconds to compile + .518 seconds to elab + 1.901 \
+        seconds to link
+    CPU Time:      0.610 seconds;       Data structure size:   1.6Mb
+
+    Returns the runtime, units as a tuple.
+    Raises RuntimeError exception if the search pattern is not found.
     """
-    # Reverse the log_text because the runtime is usually printed at the end of
-    # the file.
+    pattern = r"^CPU [tT]ime:\s*(\d+\.?\d*?)\s*(seconds|minutes|hours).*$"
     for line in reversed(log_text):
-        if "CPU Time" in line:
-            # Find pattern: CPU Time: "XXXX" seconds;.
-            m = re.search(r"(\d+\.\d+)", line)
-            if m:
-                return float(m.group(0)), "s"
-            else:
-                raise SyntaxError("Cannot find job runtime in line: " + line)
-    # We cannot find the job runtime summary line, could due to job killed,
-    # return the default value.
-    return 0, "s"
+        m = re.search(pattern, line)
+        if m:
+            return float(m.group(1)), m.group(2)[0]
+    raise RuntimeError("Job runtime not found in the log.")
 
 
 def xcelium_job_runtime(log_text: List) -> Tuple[float, str]:
-    """Capture and return the job_runtime along with its units from Xcelium simulator.
+    """Returns the Xcelium job runtime (wall clock time) along with its units.
 
-    Example of Xcelium job_runtime:
-      'TOOL: xrun(64) 21.09-s006: Exiting on Jul 22, 2022 at 13:38:48 PDT (total: 00:00:02)'
-    Raises SyntaxError exception if we find the specific line but fail to parse the
-    actual number with unit.
+    Search pattern example:
+    TOOL:	xrun(64)	21.09-s006: Exiting on Aug 01, 2022 at 00:21:18 PDT \
+        (total: 00:00:05)
+
+    Returns the runtime, units as a tuple.
+    Raises RuntimeError exception if the search pattern is not found.
     """
-    # Reverse the log_text because the runtime is usually printed at the end of
-    # the file.
+    pattern = (r"^TOOL:\s*xrun.*: Exiting on .*\(total:\s*(\d+):(\d+):(\d+)\)"
+               r"\s*$")
     for line in reversed(log_text):
-        if "total:" in line and "TOOL" in line:
-            # Find pattern: TOOL .. (total: "XX":"XX":"XX")
-            m = re.search(r"total:.*(\d+):(\d+):(\d+)", line)
-            if m:
-                # Convert time format: HR:MIN:SEC to seconds.
-                val = re.findall(r"\d+", m.group(0))
-                job_runtime = (int(val[0]) * 60 + int(val[1])) * 60 + int(val[2])
-                return job_runtime, "s"
-            else:
-                raise SyntaxError("Cannot find job runtime in line: " + line)
-    # We cannot find the job runtime summary line, could due to job killed,
-    # return the default value.
-    return 0, "s"
+        m = re.search(pattern, line)
+        if m:
+            t = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
+            return t, "s"
+    raise RuntimeError("Job runtime not found in the log.")
 
 
 def get_simulated_time(log_text: List, tool: str) -> Tuple[float, str]:
-    """Returns the job simulated time along with its units.
+    """Returns the simulated time along with its units.
+
+    EDA tools indicate how long the design was simulated for in the log file.
     This method invokes the tool specific method which parses the log text and
-    returns the simulated time as a floating point value followed by its units as a tuple.
+    returns the simulated time as a floating point value followed by its
+    units (typically, pico|nano|micro|milliseconds) as a tuple.
+
     `log_text` is the job's log file contents as a list of lines.
     `tool` is the EDA tool used to run the job.
     Returns the simulated, units as a tuple.
     Raises NotImplementedError exception if the EDA tool is not supported.
     """
-
     if tool == 'xcelium':
         return xcelium_simulated_time(log_text)
     elif tool == 'vcs':
         return vcs_simulated_time(log_text)
     else:
-        raise NotImplementedError("{tool} is unsupported for run times extraction.")
+        raise NotImplementedError(f"{tool} is unsupported for simulated time "
+                                  "extraction.")
 
 
 def xcelium_simulated_time(log_text: List) -> Tuple[float, str]:
-    """Capture and return the simulated_time along with its units from Xcelium simulator.
+    """Returns the Xcelium simulated time along with its units.
 
-    Example of Xcelium simulated_time:
-      'Simulation complete via $finish(2) at time 43361308 PS + 58'
-    Raises SyntaxError exception if we find the specific line but fail to parse the
-    actual number with unit.
+    Search pattern example:
+    Simulation complete via $finish(2) at time 11724965 PS + 13
+
+    Returns the simulated time, units as a tuple.
+    Raises RuntimeError exception if the search pattern is not found.
     """
-
-    # Reverse the log_text because Xcelium does not print out the total
-    # simulation time in summary, so we parse the log in reversed order to get
-    # the last simulation timestamp.
+    pattern = r"^Simulation complete .* at time (\d+\.?\d*?)\s*(.?[sS]).*$"
     for line in reversed(log_text):
-        if "Simulation complete via" in line:
-            m = re.search(r"at time.*(\b\d+).+(\b[A-Z]+)", line)
-            if m:
-                return int(m.group(1)), m.group(2).lower()
-            else:
-                raise SyntaxError("Cannot find the simulation time in line: " + line)
-    # We cannot find the job simulated time summary line, could due to job killed,
-    # return the default value.
-    return 0, "s"
+        m = re.search(pattern, line)
+        if m:
+            return float(m.group(1)), m.group(2).lower()
+    raise RuntimeError("Simulated time not found in the log.")
 
 
 def vcs_simulated_time(log_text: List) -> Tuple[float, str]:
-    """Capture and return the simulated_time along with its units from VCS simulator.
+    """Returns the VCS simulated time along with its units.
 
-    Example of VCS simulated_time: '$finish at simulation time 522104678 ps'
-    Raises SyntaxError exception if we find the specific line but fail to parse the
-    actual number with unit.
+    Search pattern example:
+               V C S   S i m u l a t i o n   R e p o r t
+    Time: 12241752 ps
+
+    Returns the simulated time, units as a tuple.
+    Raises RuntimeError exception if the search pattern is not found.
     """
-
-    # Reverse the log_text because the simulation time is usually printed at the end of
-    # the file.
+    pattern = r"^Time:\s*(\d+\.?\d*?)\s*(.?[sS])\s*$"
+    next_line = ""
     for line in reversed(log_text):
-        if "finish at simulation time" in line:
-            m = re.search(r"(\d+).+(\b[a-z]+)", line)
+        if "V C S   S i m u l a t i o n   R e p o r t" in line:
+            m = re.search(pattern, next_line)
             if m:
-                return int(m.group(1)), m.group(2)
-            else:
-                raise SyntaxError("Cannot find the simulation time in line: " + line)
-    # We cannot find the job simulated time summary line, could due to job killed,
-    # return the default value.
-    return 0, "s"
+                return float(m.group(1)), m.group(2).lower()
+        next_line = line
+    raise RuntimeError("Simulated time not found in the log.")