| # 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 sys | |
| from utils import VERBOSE | |
| class Modes(): | |
| """ | |
| Abstraction for specifying collection of options called as 'modes'. This is | |
| the base class which is extended for run_modes, build_modes, tests and regressions. | |
| """ | |
| def self_str(self): | |
| ''' | |
| This is used to construct the string representation of the entire class object. | |
| ''' | |
| tname = "" | |
| if self.type != "": | |
| tname = self.type + "_" | |
| if self.mname != "": | |
| tname += self.mname | |
| if log.getLogger().isEnabledFor(VERBOSE): | |
| return "\n<---" + tname + ":\n" + pprint.pformat(self.__dict__) + \ | |
| "\n--->\n" | |
| else: | |
| return tname + ":" + self.name | |
| def __str__(self): | |
| return self.self_str() | |
| def __repr__(self): | |
| return self.self_str() | |
| def __init__(self, mdict): | |
| keys = mdict.keys() | |
| attrs = self.__dict__.keys() | |
| if 'name' not in keys: | |
| log.error("Key \"name\" missing in mode %s", mdict) | |
| sys.exit(1) | |
| if not hasattr(self, "type"): | |
| log.fatal("Key \"type\" is missing or invalid") | |
| sys.exit(1) | |
| if not hasattr(self, "mname"): | |
| self.mname = "" | |
| for key in keys: | |
| if key not in attrs: | |
| log.error("Key %s in %s is invalid", key, mdict) | |
| sys.exit(1) | |
| setattr(self, key, mdict[key]) | |
| def get_sub_modes(self): | |
| sub_modes = [] | |
| if hasattr(self, "en_" + self.type + "_modes"): | |
| sub_modes = getattr(self, "en_" + self.type + "_modes") | |
| return sub_modes | |
| def set_sub_modes(self, sub_modes): | |
| setattr(self, "en_" + self.type + "_modes", sub_modes) | |
| def merge_mode(self, mode): | |
| ''' | |
| Merge a new mode with self. | |
| Merge sub mode specified with 'en_*_modes with self. | |
| ''' | |
| sub_modes = self.get_sub_modes() | |
| is_sub_mode = mode.name in sub_modes | |
| if not mode.name == self.name and not is_sub_mode: | |
| return False | |
| # Merge attributes in self with attributes in mode arg, since they are | |
| # the same mode but set in separate files, or a sub-mode. | |
| for attr, self_attr_val in self.__dict__.items(): | |
| mode_attr_val = getattr(mode, attr, None) | |
| # If sub-mode, skip the name fields - they could differ. | |
| if is_sub_mode and attr in ['name', 'mname']: | |
| continue | |
| # If mode's value is None, then nothing to do here. | |
| if mode_attr_val is None: | |
| continue | |
| # If self value is None, then replace with mode's value. | |
| if self_attr_val is None: | |
| setattr(self, attr, mode_attr_val) | |
| continue | |
| # If they are equal, then nothing to do here. | |
| if self_attr_val == mode_attr_val: | |
| continue | |
| # Extend if they are both lists. | |
| if isinstance(self_attr_val, list): | |
| assert isinstance(mode_attr_val, list) | |
| self_attr_val.extend(mode_attr_val) | |
| continue | |
| # If the current val is default, replace with new. | |
| scalar_types = {str: "", int: -1} | |
| default_val = scalar_types.get(type(self_attr_val)) | |
| if type(self_attr_val) in scalar_types.keys( | |
| ) and self_attr_val == default_val: | |
| setattr(self, attr, mode_attr_val) | |
| continue | |
| # Check if their types are compatible. | |
| if type(self_attr_val) != type(mode_attr_val): | |
| log.error( | |
| "Mode %s cannot be merged into %s due to a conflict " | |
| "(type mismatch): %s: {%s(%s), %s(%s)}", mode.name, | |
| self.name, attr, str(self_attr_val), | |
| str(type(self_attr_val)), str(mode_attr_val), | |
| str(type(mode_attr_val))) | |
| sys.exit(1) | |
| # Check if they are different non-default values. | |
| if self_attr_val != default_val and mode_attr_val != default_val: | |
| log.error( | |
| "Mode %s cannot be merged into %s due to a conflict " | |
| "(unable to pick one from different values): " | |
| "%s: {%s, %s}", mode.name, self.name, attr, | |
| str(self_attr_val), str(mode_attr_val)) | |
| sys.exit(1) | |
| # Check newly appended sub_modes, remove 'self' and duplicates | |
| sub_modes = self.get_sub_modes() | |
| if sub_modes != []: | |
| new_sub_modes = [] | |
| for sub_mode in sub_modes: | |
| if self.name != sub_mode and sub_mode not in new_sub_modes: | |
| new_sub_modes.append(sub_mode) | |
| self.set_sub_modes(new_sub_modes) | |
| return True | |
| @staticmethod | |
| def create_modes(ModeType, mdicts): | |
| ''' | |
| Create modes of type ModeType from a given list of raw dicts | |
| Process dependencies. | |
| Return a list of modes objects. | |
| ''' | |
| def merge_sub_modes(mode, parent, objs): | |
| # Check if there are modes available to merge | |
| sub_modes = mode.get_sub_modes() | |
| if sub_modes == []: | |
| return | |
| # Set parent if it is None. If not, check cyclic dependency | |
| if parent is None: | |
| parent = mode | |
| else: | |
| if mode.name == parent.name: | |
| log.error("Cyclic dependency when processing mode \"%s\"", | |
| mode.name) | |
| sys.exit(1) | |
| for sub_mode in sub_modes: | |
| # Find the sub_mode obj from str | |
| found = False | |
| for obj in objs: | |
| if sub_mode == obj.name: | |
| # First recursively merge the sub_modes | |
| merge_sub_modes(obj, parent, objs) | |
| # Now merge the sub mode with mode | |
| mode.merge_mode(obj) | |
| found = True | |
| break | |
| if not found: | |
| log.error( | |
| "Sub mode \"%s\" added to mode \"%s\" was not found!", | |
| sub_mode, mode.name) | |
| sys.exit(1) | |
| modes_objs = [] | |
| # create a default mode if available | |
| default_mode = ModeType.get_default_mode() | |
| if default_mode is not None: | |
| modes_objs.append(default_mode) | |
| # Process list of raw dicts that represent the modes | |
| # Pass 1: Create unique set of modes by merging modes with the same name | |
| for mdict in mdicts: | |
| # Create a new item | |
| new_mode_merged = False | |
| new_mode = ModeType(mdict) | |
| for mode in modes_objs: | |
| # Merge new one with existing if available | |
| if mode.name == new_mode.name: | |
| mode.merge_mode(new_mode) | |
| new_mode_merged = True | |
| break | |
| # Add the new mode to the list if not already appended | |
| if not new_mode_merged: | |
| modes_objs.append(new_mode) | |
| ModeType.item_names.append(new_mode.name) | |
| # Pass 2: Recursively expand sub modes within parent modes | |
| for mode in modes_objs: | |
| merge_sub_modes(mode, None, modes_objs) | |
| # Return the list of objects | |
| return modes_objs | |
| @staticmethod | |
| def get_default_mode(ModeType): | |
| return None | |
| @staticmethod | |
| def find_mode(mode_name, modes): | |
| ''' | |
| Given a mode_name in string, go through list of modes and return the mode with | |
| the string that matches. Thrown an error and return None if nothing was found. | |
| ''' | |
| for mode in modes: | |
| if mode_name == mode.name: | |
| return mode | |
| return None | |
| @staticmethod | |
| def find_and_merge_modes(mode, mode_names, modes, merge_modes=True): | |
| ''' | |
| ''' | |
| found_mode_objs = [] | |
| for mode_name in mode_names: | |
| sub_mode = Modes.find_mode(mode_name, modes) | |
| if sub_mode is not None: | |
| found_mode_objs.append(sub_mode) | |
| if merge_modes is True: | |
| mode.merge_mode(sub_mode) | |
| else: | |
| log.error("Mode \"%s\" enabled within mode \"%s\" not found!", | |
| mode_name, mode.name) | |
| sys.exit(1) | |
| return found_mode_objs | |
| class BuildModes(Modes): | |
| """ | |
| Build modes. | |
| """ | |
| # Maintain a list of build_modes str | |
| item_names = [] | |
| def __init__(self, bdict): | |
| self.name = "" | |
| self.type = "build" | |
| if not hasattr(self, "mname"): | |
| self.mname = "mode" | |
| self.is_sim_mode = 0 | |
| self.pre_build_cmds = [] | |
| self.post_build_cmds = [] | |
| self.en_build_modes = [] | |
| self.build_opts = [] | |
| self.pre_run_cmds = [] | |
| self.post_run_cmds = [] | |
| self.run_opts = [] | |
| self.sw_images = [] | |
| super().__init__(bdict) | |
| self.en_build_modes = list(set(self.en_build_modes)) | |
| @staticmethod | |
| def get_default_mode(): | |
| return BuildModes({"name": "default"}) | |
| class RunModes(Modes): | |
| """ | |
| Run modes. | |
| """ | |
| # Maintain a list of run_modes str | |
| item_names = [] | |
| def __init__(self, rdict): | |
| self.name = "" | |
| self.type = "run" | |
| if not hasattr(self, "mname"): | |
| self.mname = "mode" | |
| self.reseed = None | |
| self.pre_run_cmds = [] | |
| self.post_run_cmds = [] | |
| self.en_run_modes = [] | |
| self.run_opts = [] | |
| self.uvm_test = "" | |
| self.uvm_test_seq = "" | |
| self.build_mode = "" | |
| self.sw_images = [] | |
| self.sw_build_device = "" | |
| super().__init__(rdict) | |
| self.en_run_modes = list(set(self.en_run_modes)) | |
| @staticmethod | |
| def get_default_mode(): | |
| return None | |
| class Tests(RunModes): | |
| """ | |
| Abstraction for tests. The RunModes abstraction can be reused here with a few | |
| modifications. | |
| """ | |
| # Maintain a list of tests str | |
| item_names = [] | |
| # TODO: This info should be passed via hjson | |
| defaults = { | |
| "reseed": None, | |
| "uvm_test": "", | |
| "uvm_test_seq": "", | |
| "build_mode": "", | |
| "sw_images": [], | |
| "sw_build_device": "", | |
| } | |
| def __init__(self, tdict): | |
| if not hasattr(self, "mname"): | |
| self.mname = "test" | |
| super().__init__(tdict) | |
| @staticmethod | |
| def create_tests(tdicts, sim_cfg): | |
| ''' | |
| Create Tests from a given list of raw dicts. | |
| TODO: enhance the raw dict to include file scoped defaults. | |
| Process enabled run modes and the set build mode. | |
| Return a list of test objects. | |
| ''' | |
| def get_pruned_en_run_modes(test_en_run_modes, global_en_run_modes): | |
| pruned_en_run_modes = [] | |
| for test_en_run_mode in test_en_run_modes: | |
| if test_en_run_mode not in global_en_run_modes: | |
| pruned_en_run_modes.append(test_en_run_mode) | |
| return pruned_en_run_modes | |
| tests_objs = [] | |
| # Pass 1: Create unique set of tests by merging tests with the same name | |
| for tdict in tdicts: | |
| # Create a new item | |
| new_test_merged = False | |
| new_test = Tests(tdict) | |
| for test in tests_objs: | |
| # Merge new one with existing if available | |
| if test.name == new_test.name: | |
| test.merge_mode(new_test) | |
| new_test_merged = True | |
| break | |
| # Add the new test to the list if not already appended | |
| if not new_test_merged: | |
| tests_objs.append(new_test) | |
| Tests.item_names.append(new_test.name) | |
| # Pass 2: Process dependencies | |
| build_modes = [] | |
| if hasattr(sim_cfg, "build_modes"): | |
| build_modes = getattr(sim_cfg, "build_modes") | |
| run_modes = [] | |
| if hasattr(sim_cfg, "run_modes"): | |
| run_modes = getattr(sim_cfg, "run_modes") | |
| attrs = Tests.defaults | |
| for test_obj in tests_objs: | |
| # Unpack run_modes first | |
| en_run_modes = get_pruned_en_run_modes(test_obj.en_run_modes, | |
| sim_cfg.en_run_modes) | |
| Modes.find_and_merge_modes(test_obj, en_run_modes, run_modes) | |
| # Find and set the missing attributes from sim_cfg | |
| # If not found in sim_cfg either, then throw a warning | |
| # TODO: These should be file-scoped | |
| for attr in attrs.keys(): | |
| # Check if attr value is default | |
| val = getattr(test_obj, attr) | |
| default_val = attrs[attr] | |
| if val == default_val: | |
| global_val = None | |
| # Check if we can find a default in sim_cfg | |
| if hasattr(sim_cfg, attr): | |
| global_val = getattr(sim_cfg, attr) | |
| if global_val is not None and global_val != default_val: | |
| setattr(test_obj, attr, global_val) | |
| # Unpack the build mode for this test | |
| build_mode_objs = Modes.find_and_merge_modes(test_obj, | |
| [test_obj.build_mode], | |
| build_modes, | |
| merge_modes=False) | |
| test_obj.build_mode = build_mode_objs[0] | |
| # Error if set build mode is actually a sim mode | |
| if test_obj.build_mode.is_sim_mode is True: | |
| log.error( | |
| "Test \"%s\" uses build_mode %s which is actually a sim mode", | |
| test_obj.name, test_obj.build_mode.name) | |
| sys.exit(1) | |
| # Merge build_mode's params with self | |
| test_obj.pre_run_cmds.extend(test_obj.build_mode.pre_run_cmds) | |
| test_obj.post_run_cmds.extend(test_obj.build_mode.post_run_cmds) | |
| test_obj.run_opts.extend(test_obj.build_mode.run_opts) | |
| test_obj.sw_images.extend(test_obj.build_mode.sw_images) | |
| # Return the list of tests | |
| return tests_objs | |
| @staticmethod | |
| def merge_global_opts(tests, global_pre_build_cmds, global_post_build_cmds, | |
| global_build_opts, global_pre_run_cmds, | |
| global_post_run_cmds, global_run_opts, global_sw_images): | |
| processed_build_modes = [] | |
| for test in tests: | |
| if test.build_mode.name not in processed_build_modes: | |
| test.build_mode.pre_build_cmds.extend(global_pre_build_cmds) | |
| test.build_mode.post_build_cmds.extend(global_post_build_cmds) | |
| test.build_mode.build_opts.extend(global_build_opts) | |
| processed_build_modes.append(test.build_mode.name) | |
| test.pre_run_cmds.extend(global_pre_run_cmds) | |
| test.post_run_cmds.extend(global_post_run_cmds) | |
| test.run_opts.extend(global_run_opts) | |
| test.sw_images.extend(global_sw_images) | |
| class Regressions(Modes): | |
| """ | |
| Abstraction for test sets / regression sets. | |
| """ | |
| # Maintain a list of tests str | |
| item_names = [] | |
| # TODO: define __repr__ and __str__ to print list of tests if VERBOSE | |
| def __init__(self, regdict): | |
| self.name = "" | |
| self.type = "" | |
| if not hasattr(self, "mname"): | |
| self.mname = "regression" | |
| # The `tests` member is typically a list, but it defaults to None. | |
| # There are 3 possible cases after all the HJson files are parsed, when | |
| # this particular regression is supplied to be run: | |
| # | |
| # 1. `tests` == None: This is treated as "run ALL available tests". | |
| # 2. `tests` == []: No available tests to run | |
| # 3. `len(tests)` > 0: The provided set of tests are run. | |
| self.tests = None | |
| self.test_names = [] | |
| self.reseed = None | |
| self.excl_tests = [] # TODO: add support for this | |
| self.en_sim_modes = [] | |
| self.en_run_modes = [] | |
| self.pre_build_cmds = [] | |
| self.post_build_cmds = [] | |
| self.pre_run_cmds = [] | |
| self.post_run_cmds = [] | |
| self.build_opts = [] | |
| self.run_opts = [] | |
| super().__init__(regdict) | |
| @staticmethod | |
| def create_regressions(regdicts, sim_cfg, tests): | |
| ''' | |
| Create Test sets from a given list of raw dicts. | |
| Return a list of test set objects. | |
| ''' | |
| regressions_objs = [] | |
| # Pass 1: Create unique set of test sets by merging test sets with the same name | |
| for regdict in regdicts: | |
| # Create a new item | |
| new_regression_merged = False | |
| new_regression = Regressions(regdict) | |
| # Check for name conflicts with tests before merging | |
| if new_regression.name in Tests.item_names: | |
| log.error( | |
| "Test names and regression names are required to be unique. " | |
| "The regression \"%s\" bears the same name with an existing test. ", | |
| new_regression.name) | |
| sys.exit(1) | |
| for regression in regressions_objs: | |
| # Merge new one with existing if available | |
| if regression.name == new_regression.name: | |
| regression.merge_mode(new_regression) | |
| new_regression_merged = True | |
| break | |
| # Add the new test to the list if not already appended | |
| if not new_regression_merged: | |
| regressions_objs.append(new_regression) | |
| Regressions.item_names.append(new_regression.name) | |
| # Pass 2: Process dependencies | |
| build_modes = [] | |
| if hasattr(sim_cfg, "build_modes"): | |
| build_modes = getattr(sim_cfg, "build_modes") | |
| run_modes = [] | |
| if hasattr(sim_cfg, "run_modes"): | |
| run_modes = getattr(sim_cfg, "run_modes") | |
| for regression_obj in regressions_objs: | |
| # Unpack the sim modes | |
| found_sim_mode_objs = Modes.find_and_merge_modes( | |
| regression_obj, regression_obj.en_sim_modes, build_modes, | |
| False) | |
| for sim_mode_obj in found_sim_mode_objs: | |
| if sim_mode_obj.is_sim_mode == 0: | |
| log.error( | |
| "Enabled mode \"%s\" within the regression \"%s\" is not a sim mode", | |
| sim_mode_obj.name, regression_obj.name) | |
| sys.exit(1) | |
| # Check if sim_mode_obj's sub-modes are a part of regressions's | |
| # sim modes- if yes, then it will cause duplication of cmds & | |
| # opts. Throw an error and exit. | |
| for sim_mode_obj_sub in sim_mode_obj.en_build_modes: | |
| if sim_mode_obj_sub in regression_obj.en_sim_modes: | |
| log.error( | |
| "Regression \"%s\" enables sim_modes \"%s\" and \"%s\". " | |
| "The former is already a sub_mode of the latter.", | |
| regression_obj.name, sim_mode_obj_sub, | |
| sim_mode_obj.name) | |
| sys.exit(1) | |
| # Check if sim_mode_obj is also passed on the command line, in | |
| # which case, skip | |
| if sim_mode_obj.name in sim_cfg.en_build_modes: | |
| continue | |
| # Merge the build and run cmds & opts from the sim modes | |
| regression_obj.pre_build_cmds.extend( | |
| sim_mode_obj.pre_build_cmds) | |
| regression_obj.post_build_cmds.extend( | |
| sim_mode_obj.post_build_cmds) | |
| regression_obj.build_opts.extend(sim_mode_obj.build_opts) | |
| regression_obj.pre_run_cmds.extend(sim_mode_obj.pre_run_cmds) | |
| regression_obj.post_run_cmds.extend(sim_mode_obj.post_run_cmds) | |
| regression_obj.run_opts.extend(sim_mode_obj.run_opts) | |
| # Unpack the run_modes | |
| # TODO: If there are other params other than run_opts throw an | |
| # error and exit | |
| found_run_mode_objs = Modes.find_and_merge_modes( | |
| regression_obj, regression_obj.en_run_modes, run_modes, False) | |
| # Only merge the pre_run_cmds, post_run_cmds & run_opts from the | |
| # run_modes enabled | |
| for run_mode_obj in found_run_mode_objs: | |
| # Check if run_mode_obj is also passed on the command line, in | |
| # which case, skip | |
| if run_mode_obj.name in sim_cfg.en_run_modes: | |
| continue | |
| regression_obj.pre_run_cmds.extend(run_mode_obj.pre_run_cmds) | |
| regression_obj.post_run_cmds.extend(run_mode_obj.post_run_cmds) | |
| regression_obj.run_opts.extend(run_mode_obj.run_opts) | |
| # Unpack tests | |
| # If `tests` member resolves to None, then we add ALL available | |
| # tests for running the regression. | |
| if regression_obj.tests is None: | |
| log.log(VERBOSE, | |
| "Unpacking all tests in scope for regression \"%s\"", | |
| regression_obj.name) | |
| regression_obj.tests = sim_cfg.tests | |
| regression_obj.test_names = Tests.item_names | |
| else: | |
| tests_objs = [] | |
| regression_obj.test_names = regression_obj.tests | |
| for test in regression_obj.tests: | |
| test_obj = Modes.find_mode(test, sim_cfg.tests) | |
| if test_obj is None: | |
| log.error( | |
| "Test \"%s\" added to regression \"%s\" not found!", | |
| test, regression_obj.name) | |
| continue | |
| tests_objs.append(test_obj) | |
| regression_obj.tests = tests_objs | |
| # Return the list of tests | |
| return regressions_objs | |
| def merge_regression_opts(self): | |
| processed_build_modes = [] | |
| for test in self.tests: | |
| if test.build_mode.name not in processed_build_modes: | |
| test.build_mode.pre_build_cmds.extend(self.pre_build_cmds) | |
| test.build_mode.post_build_cmds.extend(self.post_build_cmds) | |
| test.build_mode.build_opts.extend(self.build_opts) | |
| processed_build_modes.append(test.build_mode.name) | |
| test.pre_run_cmds.extend(self.pre_run_cmds) | |
| test.post_run_cmds.extend(self.post_run_cmds) | |
| test.run_opts.extend(self.run_opts) | |
| # Override reseed if available. | |
| if self.reseed is not None: | |
| test.reseed = self.reseed |