Skip to content

Commit

Permalink
Select and exclude multiple scenarios (#4388)
Browse files Browse the repository at this point in the history
`molecule test` and `molecule destroy` now support specifying
`--scenario-name` multiple times to run multiple scenarios, and also
`--exclude` to exclude scenarios.

Support has also been added (along with `--all`) to `molecule check`,
`molecule cleanup`, `molecule converge`, `molecule dependency`,
`molecule idempotence`, `molecule prepare`, `molecule side-effect`,
`molecule syntax`, and `molecule verify`.
  • Loading branch information
Qalthos authored Feb 18, 2025
1 parent 833c5bc commit 9f38321
Show file tree
Hide file tree
Showing 20 changed files with 356 additions and 84 deletions.
66 changes: 51 additions & 15 deletions src/molecule/command/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,11 @@ def _setup(self) -> None:


def execute_cmdline_scenarios(
scenario_name: str | None,
scenario_names: list[str] | None,
args: MoleculeArgs,
command_args: CommandArgs,
ansible_args: tuple[str, ...] = (),
excludes: list[str] | None = None,
) -> None:
"""Execute scenario sequences based on parsed command-line arguments.
Expand All @@ -113,28 +114,33 @@ def execute_cmdline_scenarios(
to generate the scenario(s) configuration.
Args:
scenario_name: Name of scenario to run, or ``None`` to run all.
scenario_names: Name of scenarios to run, or ``None`` to run all.
args: ``args`` dict from ``click`` command context
command_args: dict of command arguments, including the target
ansible_args: Optional tuple of arguments to pass to the `ansible-playbook` command
excludes: Name of scenarios to not run.
Raises:
SystemExit: If scenario exits prematurely.
"""
glob_str = MOLECULE_GLOB
if scenario_name:
glob_str = glob_str.replace("*", scenario_name)
scenarios = molecule.scenarios.Scenarios(
get_configs(args, command_args, ansible_args, glob_str),
scenario_name,
)
if excludes is None:
excludes = []

configs: list[config.Config] = []
if scenario_names is None:
configs = [
config
for config in get_configs(args, command_args, ansible_args, MOLECULE_GLOB)
if config.scenario.name not in excludes
]
else:
# filter out excludes
scenario_names = [name for name in scenario_names if name not in excludes]
for scenario_name in scenario_names:
glob_str = MOLECULE_GLOB.replace("*", scenario_name)
configs.extend(get_configs(args, command_args, ansible_args, glob_str))

if scenario_name and scenarios:
LOG.info(
"%s scenario test matrix: %s",
scenario_name,
", ".join(scenarios.sequence(scenario_name)),
)
scenarios = _generate_scenarios(scenario_names, configs)

for scenario in scenarios:
if scenario.config.config["prerun"]:
Expand Down Expand Up @@ -171,6 +177,36 @@ def execute_cmdline_scenarios(
raise


def _generate_scenarios(
scenario_names: list[str] | None,
configs: list[config.Config],
) -> molecule.scenarios.Scenarios:
"""Generate Scenarios object from names and configs.
Args:
scenario_names: Names of scenarios to include.
configs: List of Config objects to consider.
Returns:
Combined Scenarios object.
"""
scenarios = molecule.scenarios.Scenarios(
configs,
scenario_names,
)

if scenario_names is not None:
for scenario_name in scenario_names:
if scenario_name != "*" and scenarios:
LOG.info(
"%s scenario test matrix: %s",
scenario_name,
", ".join(scenarios.sequence(scenario_name)),
)

return scenarios


def execute_subcommand(
current_config: config.Config,
subcommand_and_args: str,
Expand Down
28 changes: 24 additions & 4 deletions src/molecule/command/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,21 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--all/--no-all",
"__all",
default=False,
help="Check all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
@click.option(
"--parallel/--no-parallel",
Expand All @@ -67,7 +80,9 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
)
def check( # pragma: no cover
ctx: click.Context,
scenario_name: str,
scenario_name: list[str] | None,
exclude: list[str],
__all: bool, # noqa: FBT001
*,
parallel: bool,
) -> None:
Expand All @@ -76,13 +91,18 @@ def check( # pragma: no cover
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
__all: Whether molecule should target scenario_name or all scenarios.
parallel: Whether the scenario(s) should be run in parallel.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"parallel": parallel, "subcommand": subcommand}

if __all:
scenario_name = None

if parallel:
util.validate_parallel_cmd_args(command_args)

base.execute_cmdline_scenarios(scenario_name, args, command_args)
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)
28 changes: 24 additions & 4 deletions src/molecule/command/cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,27 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--all/--no-all",
"__all",
default=False,
help="Cleanup all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
def cleanup(
ctx: click.Context,
scenario_name: str = "default",
scenario_name: list[str] | None,
exclude: list[str],
__all: bool, # noqa: FBT001
) -> None: # pragma: no cover
"""Use the provisioner to cleanup any changes.
Expand All @@ -73,9 +88,14 @@ def cleanup(
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
__all: Whether molecule should target scenario_name or all scenarios.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand}

base.execute_cmdline_scenarios(scenario_name, args, command_args)
if __all:
scenario_name = None

base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)
28 changes: 24 additions & 4 deletions src/molecule/command/converge.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,44 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--all/--no-all",
"__all",
default=False,
help="Converge all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
@click.argument("ansible_args", nargs=-1, type=click.UNPROCESSED)
def converge(
ctx: click.Context,
scenario_name: str,
scenario_name: list[str] | None,
exclude: list[str],
__all: bool, # noqa: FBT001
ansible_args: tuple[str],
) -> None: # pragma: no cover
"""Use the provisioner to configure instances (dependency, create, prepare converge).
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
__all: Whether molecule should target scenario_name or all scenarios.
ansible_args: Arguments to forward to Ansible.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand}

base.execute_cmdline_scenarios(scenario_name, args, command_args, ansible_args)
if __all:
scenario_name = None

base.execute_cmdline_scenarios(scenario_name, args, command_args, ansible_args, exclude)
28 changes: 24 additions & 4 deletions src/molecule/command/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,49 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--driver-name",
"-d",
type=click.Choice([str(s) for s in drivers()]),
help=f"Name of driver to use. ({DEFAULT_DRIVER})",
)
@click.option(
"--all/--no-all",
"__all",
default=False,
help="Start all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
def create(
ctx: click.Context,
scenario_name: str,
scenario_name: list[str] | None,
exclude: list[str],
driver_name: str,
__all: bool, # noqa: FBT001
) -> None: # pragma: no cover
"""Use the provisioner to start the instances.
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
driver_name: Name of the Molecule driver to use.
__all: Whether molecule should target scenario_name or all scenarios.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand, "driver_name": driver_name}

base.execute_cmdline_scenarios(scenario_name, args, command_args)
if __all:
scenario_name = None

base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)
28 changes: 24 additions & 4 deletions src/molecule/command/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,41 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--all/--no-all",
"__all",
default=False,
help="Target all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
def dependency(
ctx: click.Context,
scenario_name: str,
scenario_name: list[str] | None,
exclude: list[str],
__all: bool, # noqa: FBT001
) -> None: # pragma: no cover
"""Manage the role's dependencies.
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
__all: Whether molecule should target scenario_name or all scenarios.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand}

base.execute_cmdline_scenarios(scenario_name, args, command_args)
if __all:
scenario_name = None

base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)
17 changes: 13 additions & 4 deletions src/molecule/command/destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
@click.option(
"--scenario-name",
"-s",
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
multiple=True,
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.option(
"--driver-name",
Expand All @@ -80,14 +81,21 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
default=MOLECULE_PARALLEL,
help="Destroy all scenarios. Default is False.",
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="Name of the scenario to exclude from running. May be specified multiple times.",
)
@click.option(
"--parallel/--no-parallel",
default=False,
help="Enable or disable parallel mode. Default is disabled.",
)
def destroy(
ctx: click.Context,
scenario_name: str | None,
scenario_name: list[str] | None,
exclude: list[str],
driver_name: str,
__all: bool, # noqa: FBT001
parallel: bool, # noqa: FBT001
Expand All @@ -97,6 +105,7 @@ def destroy(
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
exclude: Name of the scenarios to avoid targeting.
driver_name: Molecule driver to use.
__all: Whether molecule should target scenario_name or all scenarios.
parallel: Whether the scenario(s) should be run in parallel mode.
Expand All @@ -115,4 +124,4 @@ def destroy(
if parallel:
util.validate_parallel_cmd_args(command_args)

base.execute_cmdline_scenarios(scenario_name, args, command_args)
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)
Loading

0 comments on commit 9f38321

Please sign in to comment.