def read_actions_file( actions_csv_file, enums_by_type: Dict[str, ArgEnum], supported_platform_actions: PartialAndFullCoverageByBaseName ) -> Tuple[ActionsByName, Dict[str, str]]: """Reads the actions comma-separated-values file. If arguments are specified for an action in the file, then one action is added to the results dictionary per action_base_name + mode parameterized. A argument marked with a "*" is considered the default argument for that action. If output actions are specified for an action, then it will be a PARAMETERIZED action and the output actions will be resolved into the `Action.output_actions` field. See the README.md for more information about actions and action templates. Args: actions_csv_file: The comma-separated-values file read to parse all actions. supported_platform_actions: A dictionary of platform to the actions that are fully or partially covered on that platform. Returns (actions_by_name, action_base_name_to_default_args): actions_by_name: Index of all actions by action name. action_base_name_to_default_args: Index of action base names to the default arguments. Only populated for actions where all argument types have defaults. Raises: ValueError: The input file is invalid. """ actions_by_name: Dict[str, Action] = {} action_base_name_to_default_args: Dict[str, str] = {} action_base_names: Set[str] = set() all_ids: Set[str] = set() for i, row in enumerate(actions_csv_file): if not row: continue if row[0].startswith("#"): continue if len(row) < MIN_COLUMNS_ACTIONS_FILE: raise ValueError(f"Row {i!r} does not contain enough entries. " f"Got {row}.") action_base_name = row[0].strip() action_base_names.add(action_base_name) if not re.fullmatch(r'[a-z_]+', action_base_name): raise ValueError(f"Invald action base name {action_base_name} on " f"row {i!r}. Please use snake_case.") id_base = row[3].strip() if not id_base or id_base in all_ids: raise ValueError(f"Action id '{id_base}' on line {i!r} is " f"not populated or already used.") type = ActionType.STATE_CHANGE if action_base_name.startswith("check_"): type = ActionType.STATE_CHECK output_unresolved_action_names = [] output_actions_str = row[2].strip() if output_actions_str: type = ActionType.PARAMETERIZED # Output actions for parameterized actions can also specify (or # assume default) action arguments (e.g. `do_action(arg1)`) if the # parameterized action doesn't have a argument. However, they cannot # be fully resolved yet without reading all actions. So the # resolution must happen later. output_unresolved_action_names = [ output.strip() for output in output_actions_str.split("&") ] (partially_supported_platforms, fully_supported_platforms) = supported_platform_actions.get( action_base_name, (set(), set())) # Parse the argument types, and save the defaults if they exist. arg_types: List[ArgEnum] = [] defaults: List[str] = [] for arg_type_str in row[1].split(","): arg_type_str = arg_type_str.strip() if not arg_type_str: continue if arg_type_str not in enums_by_type: raise ValueError( f"Cannot find enum type {arg_type_str!r} on row {i!r}.") enum = enums_by_type[arg_type_str] arg_types.append(enum) if enum.default_value: defaults.append(enum.default_value) # If all arguments types have defaults, then save these defaults as the # default argument for this base action name. if len(defaults) > 0 and len(defaults) == len(arg_types): action_base_name_to_default_args[action_base_name] = ( "_".join(defaults)) # From each action row, resolve out the possible parameter arguments # and create one action per combination of arguments. all_arg_value_combinations: List[List[str]] = ( enumerate_all_argument_combinations(arg_types)) for arg_combination in all_arg_value_combinations: name = "_".join([action_base_name] + arg_combination) identifier = "".join([id_base] + arg_combination) # If the action has arguments, then modify the output actions, # and cpp method. joined_cpp_arguments = ", ".join( [f"\"{arg}\"" for arg in arg_combination]) # Convert the `cpp_method` to Pascal-case cpp_method = ''.join(word.title() for word in action_base_name.split('_')) cpp_method += "(\"" + joined_cpp_arguments + "\")" # Resolve bash-replacement for any output actions. Resolving to # canonical names is not done here because the defaults map is not # fully populated yet. output_canonical_action_names: List[str] = [] for human_friendly_action_name in output_unresolved_action_names: bash_replaced_name = resolve_bash_style_replacement( human_friendly_action_name, arg_combination) # Output actions for parameterized actions are not allowed to # use 'defaults', and the action author must explicitly # populate all arguments with bash-style replacements or static # values. output_canonical_action_names.append( human_friendly_name_to_canonical_action_name( bash_replaced_name, {})) if name in actions_by_name: raise ValueError(f"Cannot add duplicate action {name} on row " f"{i!r}") action = Action(name, action_base_name, identifier, cpp_method, type, fully_supported_platforms, partially_supported_platforms) all_ids.add(identifier) action._output_canonical_action_names = ( output_canonical_action_names) actions_by_name[action.name] = action unused_supported_actions = set( supported_platform_actions.keys()).difference(action_base_names) if unused_supported_actions: raise ValueError(f"Actions specified as suppored that are not in " f"the actions list: {unused_supported_actions}.") # Resolve the output actions for action in actions_by_name.values(): if action.type is not ActionType.PARAMETERIZED: continue assert (action._output_canonical_action_names) for canonical_name in action._output_canonical_action_names: if canonical_name in actions_by_name: action.output_actions.append(actions_by_name[canonical_name]) else: # Having this lookup fail is a feature, it allows a # parameterized action to reference output actions that might # not all support every value of the parameterized action. # When that argument is specified in a test case, then that # action would be excluded & one less test case would be # generated. logging.info(f"Output action {canonical_name} not found for " f"parameterized action {action.name}.") if not action.output_actions: raise ValueError( f"Action {action} is a parameterized action, but " f"none of it's possible parameterized actions were" f" found: {action._output_canonical_action_names}") return (actions_by_name, action_base_name_to_default_args)