| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import logging as log |
| import pprint |
| import random |
| import shlex |
| from pathlib import Path |
| |
| from LauncherFactory import get_launcher |
| from sim_utils import get_cov_summary_table |
| from tabulate import tabulate |
| from utils import (VERBOSE, clean_odirs, find_and_substitute_wildcards, |
| rm_path, subst_wildcards) |
| |
| |
| class Deploy(): |
| """ |
| Abstraction to create and maintain a runnable job (builds, runs, etc.). |
| """ |
| |
| # Indicate the target for each sub-class. |
| target = None |
| |
| # List of variable names that are to be treated as "list of commands". |
| # This tells '_construct_cmd' that these vars are lists that need to |
| # be joined with '&&' instead of a space. |
| cmds_list_vars = [] |
| |
| # Represents the weight with which a job of this target is scheduled. These |
| # initial weights set for each of the targets below are roughly inversely |
| # proportional to their average runtimes. These are subject to change in |
| # future. Lower the runtime, the higher chance the it gets scheduled. It is |
| # useful to customize this only in case of targets that may coexist at a |
| # time. |
| # TODO: Allow these to be set in the HJson. |
| weight = 1 |
| |
| def __str__(self): |
| return (pprint.pformat(self.__dict__) |
| if log.getLogger().isEnabledFor(VERBOSE) else self.full_name) |
| |
| def __init__(self, sim_cfg): |
| assert self.target is not None |
| |
| # Cross ref the whole cfg object for ease. |
| self.sim_cfg = sim_cfg |
| |
| # A list of jobs on which this job depends. |
| self.dependencies = [] |
| |
| # Indicates whether running this job requires all dependencies to pass. |
| # If this flag is set to False, any passing dependency will trigger |
| # this current job to run |
| self.needs_all_dependencies_passing = True |
| |
| # Declare attributes that need to be extracted from the HJSon cfg. |
| self._define_attrs() |
| |
| # Set class instance attributes. |
| self._set_attrs() |
| |
| # Check if all attributes that are needed are set. |
| self._check_attrs() |
| |
| # Do variable substitutions. |
| self._subst_vars() |
| |
| # List of vars required to be exported to sub-shell, as a dict. |
| self.exports = self._process_exports() |
| |
| # Construct the job's command. |
| self.cmd = self._construct_cmd() |
| |
| # Launcher instance created later using create_launcher() method. |
| self.launcher = None |
| |
| def _define_attrs(self): |
| """Defines the attributes this instance needs to have. |
| |
| These attributes are extracted from the Mode object / HJson config with |
| which this instance is created. There are two types of attributes - |
| one contributes to the generation of the command directly; the other |
| provides supplementary information pertaining to the job, such as |
| patterns that determine whether it passed or failed. These are |
| represented as dicts, whose values indicate in boolean whether the |
| extraction was successful. |
| """ |
| # These attributes are explicitly used to construct the job command. |
| self.mandatory_cmd_attrs = {} |
| |
| # These attributes may indirectly contribute to the construction of the |
| # command (through substitution vars) or other things such as pass / |
| # fail patterns. |
| self.mandatory_misc_attrs = { |
| "name": False, |
| "build_mode": False, |
| "flow_makefile": False, |
| "exports": False, |
| "dry_run": False |
| } |
| |
| # Function to parse a dict and extract the mandatory cmd and misc attrs. |
| def _extract_attrs(self, ddict): |
| """Extracts the attributes from the supplied dict. |
| |
| 'ddict' is typically either the Mode object or the entire config |
| object's dict. It is used to retrieve the instance attributes defined |
| in 'mandatory_cmd_attrs' and 'mandatory_misc_attrs'. |
| """ |
| ddict_keys = ddict.keys() |
| for key in self.mandatory_cmd_attrs.keys(): |
| if self.mandatory_cmd_attrs[key] is False: |
| if key in ddict_keys: |
| setattr(self, key, ddict[key]) |
| self.mandatory_cmd_attrs[key] = True |
| |
| for key in self.mandatory_misc_attrs.keys(): |
| if self.mandatory_misc_attrs[key] is False: |
| if key in ddict_keys: |
| setattr(self, key, ddict[key]) |
| self.mandatory_misc_attrs[key] = True |
| |
| def _set_attrs(self): |
| """Sets additional attributes. |
| |
| Invokes '_extract_attrs()' to read in all the necessary instance |
| attributes. Based on those, some additional instance attributes may |
| be derived. Those are set by this method. |
| """ |
| self._extract_attrs(self.sim_cfg.__dict__) |
| |
| # Enable GUI mode. |
| self.gui = self.sim_cfg.gui |
| |
| # Output directory where the artifacts go (used by the launcher). |
| self.odir = getattr(self, self.target + "_dir") |
| |
| # Qualified name disambiguates the instance name with other instances |
| # of the same class (example: 'uart_smoke' reseeded multiple times |
| # needs to be disambiguated using the index -> '0.uart_smoke'. |
| self.qual_name = self.name |
| |
| # Full name disambiguates across multiple cfg being run (example: |
| # 'aes:default', 'uart:default' builds. |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| |
| # Job name is used to group the job by cfg and target. The scratch path |
| # directory name is assumed to be uniquified, in case there are more |
| # than one sim_cfgs with the same name. |
| self.job_name = "{}_{}".format( |
| Path(self.sim_cfg.scratch_path).name, self.target) |
| |
| # Input directories (other than self) this job depends on. |
| self.input_dirs = [] |
| |
| # Directories touched by this job. These directories are marked |
| # becuase they are used by dependent jobs as input. |
| self.output_dirs = [self.odir] |
| |
| # Pass and fail patterns. |
| self.pass_patterns = [] |
| self.fail_patterns = [] |
| |
| def _check_attrs(self): |
| """Checks if all required class attributes are set. |
| |
| Invoked in __init__() after all attributes are extracted and set. |
| """ |
| for attr in self.mandatory_cmd_attrs.keys(): |
| if self.mandatory_cmd_attrs[attr] is False: |
| raise AttributeError("Attribute {!r} not found for " |
| "{!r}.".format(attr, self.name)) |
| |
| for attr in self.mandatory_misc_attrs.keys(): |
| if self.mandatory_misc_attrs[attr] is False: |
| raise AttributeError("Attribute {!r} not found for " |
| "{!r}.".format(attr, self.name)) |
| |
| def _subst_vars(self, ignored_subst_vars=[]): |
| """Recursively search and replace substitution variables. |
| |
| First pass: search within self dict. We ignore errors since some |
| substitions may be available in the second pass. Second pass: search |
| the entire sim_cfg object.""" |
| |
| self.__dict__ = find_and_substitute_wildcards(self.__dict__, |
| self.__dict__, |
| ignored_subst_vars, True) |
| self.__dict__ = find_and_substitute_wildcards(self.__dict__, |
| self.sim_cfg.__dict__, |
| ignored_subst_vars, |
| False) |
| |
| def _process_exports(self): |
| """Convert 'exports' as a list of dicts in the HJson to a dict. |
| |
| Exports is a list of key-value pairs that are to be exported to the |
| subprocess' environment so that the tools can lookup those options. |
| DVSim limits how the data is presented in the HJson (the value of a |
| HJson member cannot be an object). This method converts a list of dicts |
| into a dict variable, which makes it easy to merge the list of exports |
| with the subprocess' env where the ASIC tool is invoked. |
| """ |
| |
| return {k: str(v) for item in self.exports for k, v in item.items()} |
| |
| def _construct_cmd(self): |
| """Construct the command that will eventually be launched.""" |
| |
| cmd = "make -f {} {}".format(self.flow_makefile, self.target) |
| if self.dry_run is True: |
| cmd += " -n" |
| for attr in sorted(self.mandatory_cmd_attrs.keys()): |
| value = getattr(self, attr) |
| if type(value) is list: |
| # Join attributes that are list of commands with '&&' to chain |
| # them together when executed as a Make target's recipe. |
| separator = " && " if attr in self.cmds_list_vars else " " |
| value = separator.join(item.strip() for item in value) |
| if type(value) is bool: |
| value = int(value) |
| if type(value) is str: |
| value = value.strip() |
| cmd += " {}={}".format(attr, shlex.quote(value)) |
| return cmd |
| |
| def is_equivalent_job(self, item): |
| """Checks if job that would be dispatched with 'item' is equivalent to |
| 'self'. |
| |
| Determines if 'item' and 'self' would behave exactly the same way when |
| deployed. If so, then there is no point in keeping both. The caller can |
| choose to discard 'item' and pick 'self' instead. To do so, we check |
| the final resolved 'cmd' & the exports. The 'name' field will be unique |
| to 'item' and 'self', so we take that out of the comparison. |
| """ |
| if type(self) != type(item): |
| return False |
| |
| # Check if the cmd field is identical. |
| item_cmd = item.cmd.replace(item.name, self.name) |
| if self.cmd != item_cmd: |
| return False |
| |
| # Check if exports have identical set of keys. |
| if self.exports.keys() != item.exports.keys(): |
| return False |
| |
| # Check if exports have identical values. |
| for key, val in self.exports.items(): |
| item_val = item.exports[key] |
| if type(item_val) is str: |
| item_val = item_val.replace(item.name, self.name) |
| if val != item_val: |
| return False |
| |
| log.log(VERBOSE, "Deploy job \"%s\" is equivalent to \"%s\"", |
| item.name, self.name) |
| return True |
| |
| def pre_launch(self): |
| """Callback to perform additional pre-launch activities. |
| |
| This is invoked by launcher::_pre_launch(). |
| """ |
| pass |
| |
| def post_finish(self, status): |
| """Callback to perform additional post-finish activities. |
| |
| This is invoked by launcher::_post_finish(). |
| """ |
| pass |
| |
| def get_log_path(self): |
| """Returns the log file path.""" |
| |
| return "{}/{}.log".format(self.odir, self.target) |
| |
| def create_launcher(self): |
| """Creates the launcher instance. |
| |
| Note that the launcher instance for ALL jobs in the same job group must |
| be created before the Scheduler starts to dispatch one by one. |
| """ |
| # Retain the handle to self for lookup & callbacks. |
| self.launcher = get_launcher(self) |
| |
| |
| class CompileSim(Deploy): |
| """Abstraction for building the simulation executable.""" |
| |
| target = "build" |
| cmds_list_vars = ["pre_build_cmds", "post_build_cmds"] |
| weight = 5 |
| |
| def __init__(self, build_mode, sim_cfg): |
| self.build_mode_obj = build_mode |
| self.seed = sim_cfg.build_seed |
| super().__init__(sim_cfg) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| # tool srcs |
| "proj_root": False, |
| |
| # Flist gen |
| "sv_flist_gen_cmd": False, |
| "sv_flist_gen_dir": False, |
| "sv_flist_gen_opts": False, |
| |
| # Build |
| "build_dir": False, |
| "pre_build_cmds": False, |
| "build_cmd": False, |
| "build_opts": False, |
| "post_build_cmds": False, |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "cov_db_dir": False, |
| "build_pass_patterns": False, |
| "build_fail_patterns": False |
| }) |
| |
| def _set_attrs(self): |
| super()._extract_attrs(self.build_mode_obj.__dict__) |
| super()._set_attrs() |
| |
| # Dont run the compile job in GUI mode. |
| self.gui = False |
| |
| # 'build_mode' is used as a substitution variable in the HJson. |
| self.build_mode = self.name |
| self.job_name += f"_{self.build_mode}" |
| if self.sim_cfg.cov: |
| self.output_dirs += [self.cov_db_dir] |
| self.pass_patterns = self.build_pass_patterns |
| self.fail_patterns = self.build_fail_patterns |
| |
| def pre_launch(self): |
| # Delete old coverage database directories before building again. We |
| # need to do this because the build directory is not 'renewed'. |
| rm_path(self.cov_db_dir) |
| |
| |
| class CompileOneShot(Deploy): |
| """Abstraction for building the design (used by non-DV flows).""" |
| |
| target = "build" |
| |
| def __init__(self, build_mode, sim_cfg): |
| self.build_mode_obj = build_mode |
| super().__init__(sim_cfg) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| # tool srcs |
| "proj_root": False, |
| |
| # Flist gen |
| "sv_flist_gen_cmd": False, |
| "sv_flist_gen_dir": False, |
| "sv_flist_gen_opts": False, |
| |
| # Build |
| "build_dir": False, |
| "pre_build_cmds": False, |
| "build_cmd": False, |
| "build_opts": False, |
| "post_build_cmds": False, |
| "build_log": False, |
| |
| # Report processing |
| "report_cmd": False, |
| "report_opts": False |
| }) |
| |
| self.mandatory_misc_attrs.update({"build_fail_patterns": False}) |
| |
| def _set_attrs(self): |
| super()._extract_attrs(self.build_mode_obj.__dict__) |
| super()._set_attrs() |
| |
| # 'build_mode' is used as a substitution variable in the HJson. |
| self.build_mode = self.name |
| self.job_name += f"_{self.build_mode}" |
| self.fail_patterns = self.build_fail_patterns |
| |
| |
| class RunTest(Deploy): |
| """Abstraction for running tests. This is one per seed for each test.""" |
| |
| # Initial seed values when running tests (if available). |
| target = "run" |
| seeds = [] |
| fixed_seed = None |
| cmds_list_vars = ["pre_run_cmds", "post_run_cmds"] |
| |
| def __init__(self, index, test, build_job, sim_cfg): |
| self.test_obj = test |
| self.index = index |
| self.seed = RunTest.get_seed() |
| super().__init__(sim_cfg) |
| |
| if build_job is not None: |
| self.dependencies.append(build_job) |
| |
| # We did something wrong if build_mode is not the same as the build_job |
| # arg's name. |
| assert self.build_mode == build_job.name |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| # tool srcs |
| "proj_root": False, |
| "uvm_test": False, |
| "uvm_test_seq": False, |
| "sw_images": False, |
| "sw_build_device": False, |
| "sw_build_dir": False, |
| "sw_build_cmd": False, |
| "sw_build_opts": False, |
| "run_dir": False, |
| "pre_run_cmds": False, |
| "run_cmd": False, |
| "run_opts": False, |
| "post_run_cmds": False, |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "run_dir_name": False, |
| "cov_db_dir": False, |
| "cov_db_test_dir": False, |
| "run_pass_patterns": False, |
| "run_fail_patterns": False |
| }) |
| |
| def _set_attrs(self): |
| super()._extract_attrs(self.test_obj.__dict__) |
| super()._set_attrs() |
| |
| # 'test' is used as a substitution variable in the HJson. |
| self.test = self.name |
| self.build_mode = self.test_obj.build_mode.name |
| self.qual_name = self.run_dir_name + "." + str(self.seed) |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| self.job_name += f"_{self.build_mode}" |
| if self.sim_cfg.cov: |
| self.output_dirs += [self.cov_db_dir] |
| |
| # In GUI mode, the log file is not updated; hence, nothing to check. |
| if not self.gui: |
| self.pass_patterns = self.run_pass_patterns |
| self.fail_patterns = self.run_fail_patterns |
| |
| def pre_launch(self): |
| self.launcher.renew_odir = True |
| |
| def post_finish(self, status): |
| if status != 'P': |
| # Delete the coverage data if available. |
| rm_path(self.cov_db_test_dir) |
| |
| @staticmethod |
| def get_seed(): |
| # If --seeds option is passed, then those custom seeds are consumed |
| # first. If --fixed-seed <val> is also passed, the subsequent tests |
| # (once the custom seeds are consumed) will be run with the fixed seed. |
| if not RunTest.seeds: |
| if RunTest.fixed_seed: |
| return RunTest.fixed_seed |
| for i in range(1000): |
| seed = random.getrandbits(32) |
| RunTest.seeds.append(seed) |
| return RunTest.seeds.pop(0) |
| |
| |
| class CovUnr(Deploy): |
| """Abstraction for coverage UNR flow.""" |
| |
| target = "cov_unr" |
| |
| def __init__(self, sim_cfg): |
| super().__init__(sim_cfg) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| # tool srcs |
| "proj_root": False, |
| |
| # Need to generate filelist based on build mode |
| "sv_flist_gen_cmd": False, |
| "sv_flist_gen_dir": False, |
| "sv_flist_gen_opts": False, |
| "build_dir": False, |
| "cov_unr_build_cmd": False, |
| "cov_unr_build_opts": False, |
| "cov_unr_run_cmd": False, |
| "cov_unr_run_opts": False |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "cov_unr_dir": False, |
| "cov_merge_db_dir": False, |
| "build_fail_patterns": False |
| }) |
| |
| def _set_attrs(self): |
| super()._set_attrs() |
| self.qual_name = self.target |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| self.input_dirs += [self.cov_merge_db_dir] |
| |
| # Reuse the build_fail_patterns set in the HJson. |
| self.fail_patterns = self.build_fail_patterns |
| |
| |
| class CovMerge(Deploy): |
| """Abstraction for merging coverage databases.""" |
| |
| target = "cov_merge" |
| weight = 10 |
| |
| def __init__(self, run_items, sim_cfg): |
| # Construct the cov_db_dirs right away from the run_items. This is a |
| # special variable used in the HJson. |
| self.cov_db_dirs = [] |
| for run in run_items: |
| if run.cov_db_dir not in self.cov_db_dirs: |
| self.cov_db_dirs.append(run.cov_db_dir) |
| |
| # Early lookup the cov_merge_db_dir, which is a mandatory misc |
| # attribute anyway. We need it to compute additional cov db dirs. |
| self.cov_merge_db_dir = subst_wildcards("{cov_merge_db_dir}", |
| sim_cfg.__dict__) |
| |
| # Prune previous merged cov directories, keeping past 7 dbs. |
| prev_cov_db_dirs = clean_odirs(odir=self.cov_merge_db_dir, max_odirs=7) |
| |
| # If the --cov-merge-previous command line switch is passed, then |
| # merge coverage with the previous runs. |
| if sim_cfg.cov_merge_previous: |
| self.cov_db_dirs += [str(item) for item in prev_cov_db_dirs] |
| |
| super().__init__(sim_cfg) |
| self.dependencies += run_items |
| # Run coverage merge even if one test passes. |
| self.needs_all_dependencies_passing = False |
| |
| # Append cov_db_dirs to the list of exports. |
| self.exports["cov_db_dirs"] = shlex.quote(" ".join(self.cov_db_dirs)) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| "cov_merge_cmd": False, |
| "cov_merge_opts": False |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "cov_merge_dir": False, |
| "cov_merge_db_dir": False |
| }) |
| |
| def _set_attrs(self): |
| super()._set_attrs() |
| self.qual_name = self.target |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| |
| # For merging coverage db, the precise output dir is set in the HJson. |
| self.odir = self.cov_merge_db_dir |
| self.input_dirs += self.cov_db_dirs |
| self.output_dirs = [self.odir] |
| |
| |
| class CovReport(Deploy): |
| """Abstraction for coverage report generation. """ |
| |
| target = "cov_report" |
| weight = 10 |
| |
| def __init__(self, merge_job, sim_cfg): |
| super().__init__(sim_cfg) |
| self.dependencies.append(merge_job) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| "cov_report_cmd": False, |
| "cov_report_opts": False |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "cov_report_dir": False, |
| "cov_merge_db_dir": False, |
| "cov_report_txt": False |
| }) |
| |
| def _set_attrs(self): |
| super()._set_attrs() |
| self.qual_name = self.target |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| |
| # Keep track of coverage results, once the job is finished. |
| self.cov_total = "" |
| self.cov_results = "" |
| |
| def post_finish(self, status): |
| """Extract the coverage results summary for the dashboard. |
| |
| If the extraction fails, an appropriate exception is raised, which must |
| be caught by the caller to mark the job as a failure. |
| """ |
| |
| if self.dry_run or status != 'P': |
| return |
| |
| results, self.cov_total = get_cov_summary_table( |
| self.cov_report_txt, self.sim_cfg.tool) |
| |
| colalign = (("center", ) * len(results[0])) |
| self.cov_results = tabulate(results, |
| headers="firstrow", |
| tablefmt="pipe", |
| colalign=colalign) |
| |
| |
| class CovAnalyze(Deploy): |
| """Abstraction for running the coverage analysis tool.""" |
| |
| target = "cov_analyze" |
| |
| def __init__(self, sim_cfg): |
| # Enforce GUI mode for coverage analysis. |
| sim_cfg.gui = True |
| super().__init__(sim_cfg) |
| |
| def _define_attrs(self): |
| super()._define_attrs() |
| self.mandatory_cmd_attrs.update({ |
| # tool srcs |
| "proj_root": False, |
| "cov_analyze_cmd": False, |
| "cov_analyze_opts": False |
| }) |
| |
| self.mandatory_misc_attrs.update({ |
| "cov_analyze_dir": False, |
| "cov_merge_db_dir": False |
| }) |
| |
| def _set_attrs(self): |
| super()._set_attrs() |
| self.qual_name = self.target |
| self.full_name = self.sim_cfg.name + ":" + self.qual_name |
| self.input_dirs += [self.cov_merge_db_dir] |