# 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 # only merge the lists; if strs are different, then throw an error attrs = self.__dict__.keys() for attr in attrs: # merge lists together self_attr_val = getattr(self, attr) mode_attr_val = getattr(mode, attr) if type(self_attr_val) is list: self_attr_val.extend(mode_attr_val) setattr(self, attr, self_attr_val) elif not is_sub_mode or attr not in ["name", "mname"]: self.check_conflict(mode.name, attr, mode_attr_val) # 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 def check_conflict(self, name, attr, mode_attr_val): self_attr_val = getattr(self, attr) if self_attr_val == mode_attr_val: return if mode_attr_val is None: # No override here return default_val = None if type(self_attr_val) is int: default_val = -1 elif type(self_attr_val) is str: default_val = "" if self_attr_val != default_val and mode_attr_val != default_val: log.error( "mode %s cannot be merged into %s due to conflicting %s {%s, %s}", name, self.name, attr, str(self_attr_val), str(mode_attr_val)) sys.exit(1) elif self_attr_val == default_val: self_attr_val = mode_attr_val setattr(self, attr, self_attr_val) @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.build_opts = [] self.run_opts = [] self.en_build_modes = [] 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.run_opts = [] self.uvm_test = "" self.uvm_test_seq = "" self.build_mode = "" self.en_run_modes = [] self.sw_test = "" self.sw_test_is_prebuilt = "" 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_test": "", "sw_test_is_prebuilt": "", "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 run_opts with self test_obj.run_opts.extend(test_obj.build_mode.run_opts) # Return the list of tests return tests_objs @staticmethod def merge_global_opts(tests, global_build_opts, global_run_opts): processed_build_modes = [] for test in tests: if test.build_mode.name not in processed_build_modes: test.build_mode.build_opts.extend(global_build_opts) processed_build_modes.append(test.build_mode.name) test.run_opts.extend(global_run_opts) 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" self.tests = [] self.reseed = None self.test_names = [] self.excl_tests = [] # TODO: add support for this self.en_sim_modes = [] self.en_run_modes = [] 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 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 opts from the sim modes regression_obj.build_opts.extend(sim_mode_obj.build_opts) 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 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.run_opts.extend(run_mode_obj.run_opts) # Unpack tests if regression_obj.tests == []: 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.build_opts.extend(self.build_opts) processed_build_modes.append(test.build_mode.name) test.run_opts.extend(self.run_opts) # Override reseed if available. if self.reseed is not None: test.reseed = self.reseed