Example #1
0
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)