Skip to content

Commit 9f38321

Browse files
authored
Select and exclude multiple scenarios (#4388)
`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`.
1 parent 833c5bc commit 9f38321

20 files changed

+356
-84
lines changed

src/molecule/command/base.py

+51-15
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,11 @@ def _setup(self) -> None:
9999

100100

101101
def execute_cmdline_scenarios(
102-
scenario_name: str | None,
102+
scenario_names: list[str] | None,
103103
args: MoleculeArgs,
104104
command_args: CommandArgs,
105105
ansible_args: tuple[str, ...] = (),
106+
excludes: list[str] | None = None,
106107
) -> None:
107108
"""Execute scenario sequences based on parsed command-line arguments.
108109
@@ -113,28 +114,33 @@ def execute_cmdline_scenarios(
113114
to generate the scenario(s) configuration.
114115
115116
Args:
116-
scenario_name: Name of scenario to run, or ``None`` to run all.
117+
scenario_names: Name of scenarios to run, or ``None`` to run all.
117118
args: ``args`` dict from ``click`` command context
118119
command_args: dict of command arguments, including the target
119120
ansible_args: Optional tuple of arguments to pass to the `ansible-playbook` command
121+
excludes: Name of scenarios to not run.
120122
121123
Raises:
122124
SystemExit: If scenario exits prematurely.
123125
"""
124-
glob_str = MOLECULE_GLOB
125-
if scenario_name:
126-
glob_str = glob_str.replace("*", scenario_name)
127-
scenarios = molecule.scenarios.Scenarios(
128-
get_configs(args, command_args, ansible_args, glob_str),
129-
scenario_name,
130-
)
126+
if excludes is None:
127+
excludes = []
128+
129+
configs: list[config.Config] = []
130+
if scenario_names is None:
131+
configs = [
132+
config
133+
for config in get_configs(args, command_args, ansible_args, MOLECULE_GLOB)
134+
if config.scenario.name not in excludes
135+
]
136+
else:
137+
# filter out excludes
138+
scenario_names = [name for name in scenario_names if name not in excludes]
139+
for scenario_name in scenario_names:
140+
glob_str = MOLECULE_GLOB.replace("*", scenario_name)
141+
configs.extend(get_configs(args, command_args, ansible_args, glob_str))
131142

132-
if scenario_name and scenarios:
133-
LOG.info(
134-
"%s scenario test matrix: %s",
135-
scenario_name,
136-
", ".join(scenarios.sequence(scenario_name)),
137-
)
143+
scenarios = _generate_scenarios(scenario_names, configs)
138144

139145
for scenario in scenarios:
140146
if scenario.config.config["prerun"]:
@@ -171,6 +177,36 @@ def execute_cmdline_scenarios(
171177
raise
172178

173179

180+
def _generate_scenarios(
181+
scenario_names: list[str] | None,
182+
configs: list[config.Config],
183+
) -> molecule.scenarios.Scenarios:
184+
"""Generate Scenarios object from names and configs.
185+
186+
Args:
187+
scenario_names: Names of scenarios to include.
188+
configs: List of Config objects to consider.
189+
190+
Returns:
191+
Combined Scenarios object.
192+
"""
193+
scenarios = molecule.scenarios.Scenarios(
194+
configs,
195+
scenario_names,
196+
)
197+
198+
if scenario_names is not None:
199+
for scenario_name in scenario_names:
200+
if scenario_name != "*" and scenarios:
201+
LOG.info(
202+
"%s scenario test matrix: %s",
203+
scenario_name,
204+
", ".join(scenarios.sequence(scenario_name)),
205+
)
206+
207+
return scenarios
208+
209+
174210
def execute_subcommand(
175211
current_config: config.Config,
176212
subcommand_and_args: str,

src/molecule/command/check.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,21 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
5757
@click.option(
5858
"--scenario-name",
5959
"-s",
60-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
61-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
60+
multiple=True,
61+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
62+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
63+
)
64+
@click.option(
65+
"--all/--no-all",
66+
"__all",
67+
default=False,
68+
help="Check all scenarios. Default is False.",
69+
)
70+
@click.option(
71+
"--exclude",
72+
"-e",
73+
multiple=True,
74+
help="Name of the scenario to exclude from running. May be specified multiple times.",
6275
)
6376
@click.option(
6477
"--parallel/--no-parallel",
@@ -67,7 +80,9 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
6780
)
6881
def check( # pragma: no cover
6982
ctx: click.Context,
70-
scenario_name: str,
83+
scenario_name: list[str] | None,
84+
exclude: list[str],
85+
__all: bool, # noqa: FBT001
7186
*,
7287
parallel: bool,
7388
) -> None:
@@ -76,13 +91,18 @@ def check( # pragma: no cover
7691
Args:
7792
ctx: Click context object holding commandline arguments.
7893
scenario_name: Name of the scenario to target.
94+
exclude: Name of the scenarios to avoid targeting.
95+
__all: Whether molecule should target scenario_name or all scenarios.
7996
parallel: Whether the scenario(s) should be run in parallel.
8097
"""
8198
args: MoleculeArgs = ctx.obj.get("args")
8299
subcommand = base._get_subcommand(__name__) # noqa: SLF001
83100
command_args: CommandArgs = {"parallel": parallel, "subcommand": subcommand}
84101

102+
if __all:
103+
scenario_name = None
104+
85105
if parallel:
86106
util.validate_parallel_cmd_args(command_args)
87107

88-
base.execute_cmdline_scenarios(scenario_name, args, command_args)
108+
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)

src/molecule/command/cleanup.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,27 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
5959
@click.option(
6060
"--scenario-name",
6161
"-s",
62-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
63-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
62+
multiple=True,
63+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
64+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
65+
)
66+
@click.option(
67+
"--all/--no-all",
68+
"__all",
69+
default=False,
70+
help="Cleanup all scenarios. Default is False.",
71+
)
72+
@click.option(
73+
"--exclude",
74+
"-e",
75+
multiple=True,
76+
help="Name of the scenario to exclude from running. May be specified multiple times.",
6477
)
6578
def cleanup(
6679
ctx: click.Context,
67-
scenario_name: str = "default",
80+
scenario_name: list[str] | None,
81+
exclude: list[str],
82+
__all: bool, # noqa: FBT001
6883
) -> None: # pragma: no cover
6984
"""Use the provisioner to cleanup any changes.
7085
@@ -73,9 +88,14 @@ def cleanup(
7388
Args:
7489
ctx: Click context object holding commandline arguments.
7590
scenario_name: Name of the scenario to target.
91+
exclude: Name of the scenarios to avoid targeting.
92+
__all: Whether molecule should target scenario_name or all scenarios.
7693
"""
7794
args: MoleculeArgs = ctx.obj.get("args")
7895
subcommand = base._get_subcommand(__name__) # noqa: SLF001
7996
command_args: CommandArgs = {"subcommand": subcommand}
8097

81-
base.execute_cmdline_scenarios(scenario_name, args, command_args)
98+
if __all:
99+
scenario_name = None
100+
101+
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)

src/molecule/command/converge.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -55,24 +55,44 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
5555
@click.option(
5656
"--scenario-name",
5757
"-s",
58-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
59-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
58+
multiple=True,
59+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
60+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
61+
)
62+
@click.option(
63+
"--all/--no-all",
64+
"__all",
65+
default=False,
66+
help="Converge all scenarios. Default is False.",
67+
)
68+
@click.option(
69+
"--exclude",
70+
"-e",
71+
multiple=True,
72+
help="Name of the scenario to exclude from running. May be specified multiple times.",
6073
)
6174
@click.argument("ansible_args", nargs=-1, type=click.UNPROCESSED)
6275
def converge(
6376
ctx: click.Context,
64-
scenario_name: str,
77+
scenario_name: list[str] | None,
78+
exclude: list[str],
79+
__all: bool, # noqa: FBT001
6580
ansible_args: tuple[str],
6681
) -> None: # pragma: no cover
6782
"""Use the provisioner to configure instances (dependency, create, prepare converge).
6883
6984
Args:
7085
ctx: Click context object holding commandline arguments.
7186
scenario_name: Name of the scenario to target.
87+
exclude: Name of the scenarios to avoid targeting.
88+
__all: Whether molecule should target scenario_name or all scenarios.
7289
ansible_args: Arguments to forward to Ansible.
7390
"""
7491
args: MoleculeArgs = ctx.obj.get("args")
7592
subcommand = base._get_subcommand(__name__) # noqa: SLF001
7693
command_args: CommandArgs = {"subcommand": subcommand}
7794

78-
base.execute_cmdline_scenarios(scenario_name, args, command_args, ansible_args)
95+
if __all:
96+
scenario_name = None
97+
98+
base.execute_cmdline_scenarios(scenario_name, args, command_args, ansible_args, exclude)

src/molecule/command/create.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -65,29 +65,49 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
6565
@click.option(
6666
"--scenario-name",
6767
"-s",
68-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
69-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
68+
multiple=True,
69+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
70+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
7071
)
7172
@click.option(
7273
"--driver-name",
7374
"-d",
7475
type=click.Choice([str(s) for s in drivers()]),
7576
help=f"Name of driver to use. ({DEFAULT_DRIVER})",
7677
)
78+
@click.option(
79+
"--all/--no-all",
80+
"__all",
81+
default=False,
82+
help="Start all scenarios. Default is False.",
83+
)
84+
@click.option(
85+
"--exclude",
86+
"-e",
87+
multiple=True,
88+
help="Name of the scenario to exclude from running. May be specified multiple times.",
89+
)
7790
def create(
7891
ctx: click.Context,
79-
scenario_name: str,
92+
scenario_name: list[str] | None,
93+
exclude: list[str],
8094
driver_name: str,
95+
__all: bool, # noqa: FBT001
8196
) -> None: # pragma: no cover
8297
"""Use the provisioner to start the instances.
8398
8499
Args:
85100
ctx: Click context object holding commandline arguments.
86101
scenario_name: Name of the scenario to target.
102+
exclude: Name of the scenarios to avoid targeting.
87103
driver_name: Name of the Molecule driver to use.
104+
__all: Whether molecule should target scenario_name or all scenarios.
88105
"""
89106
args: MoleculeArgs = ctx.obj.get("args")
90107
subcommand = base._get_subcommand(__name__) # noqa: SLF001
91108
command_args: CommandArgs = {"subcommand": subcommand, "driver_name": driver_name}
92109

93-
base.execute_cmdline_scenarios(scenario_name, args, command_args)
110+
if __all:
111+
scenario_name = None
112+
113+
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)

src/molecule/command/dependency.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,41 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
5454
@click.option(
5555
"--scenario-name",
5656
"-s",
57-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
58-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
57+
multiple=True,
58+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
59+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
60+
)
61+
@click.option(
62+
"--all/--no-all",
63+
"__all",
64+
default=False,
65+
help="Target all scenarios. Default is False.",
66+
)
67+
@click.option(
68+
"--exclude",
69+
"-e",
70+
multiple=True,
71+
help="Name of the scenario to exclude from running. May be specified multiple times.",
5972
)
6073
def dependency(
6174
ctx: click.Context,
62-
scenario_name: str,
75+
scenario_name: list[str] | None,
76+
exclude: list[str],
77+
__all: bool, # noqa: FBT001
6378
) -> None: # pragma: no cover
6479
"""Manage the role's dependencies.
6580
6681
Args:
6782
ctx: Click context object holding commandline arguments.
6883
scenario_name: Name of the scenario to target.
84+
exclude: Name of the scenarios to avoid targeting.
85+
__all: Whether molecule should target scenario_name or all scenarios.
6986
"""
7087
args: MoleculeArgs = ctx.obj.get("args")
7188
subcommand = base._get_subcommand(__name__) # noqa: SLF001
7289
command_args: CommandArgs = {"subcommand": subcommand}
7390

74-
base.execute_cmdline_scenarios(scenario_name, args, command_args)
91+
if __all:
92+
scenario_name = None
93+
94+
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)

src/molecule/command/destroy.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
6565
@click.option(
6666
"--scenario-name",
6767
"-s",
68-
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
69-
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
68+
multiple=True,
69+
default=[base.MOLECULE_DEFAULT_SCENARIO_NAME],
70+
help=f"Name of the scenario to target. May be specified multiple times. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
7071
)
7172
@click.option(
7273
"--driver-name",
@@ -80,14 +81,21 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
8081
default=MOLECULE_PARALLEL,
8182
help="Destroy all scenarios. Default is False.",
8283
)
84+
@click.option(
85+
"--exclude",
86+
"-e",
87+
multiple=True,
88+
help="Name of the scenario to exclude from running. May be specified multiple times.",
89+
)
8390
@click.option(
8491
"--parallel/--no-parallel",
8592
default=False,
8693
help="Enable or disable parallel mode. Default is disabled.",
8794
)
8895
def destroy(
8996
ctx: click.Context,
90-
scenario_name: str | None,
97+
scenario_name: list[str] | None,
98+
exclude: list[str],
9199
driver_name: str,
92100
__all: bool, # noqa: FBT001
93101
parallel: bool, # noqa: FBT001
@@ -97,6 +105,7 @@ def destroy(
97105
Args:
98106
ctx: Click context object holding commandline arguments.
99107
scenario_name: Name of the scenario to target.
108+
exclude: Name of the scenarios to avoid targeting.
100109
driver_name: Molecule driver to use.
101110
__all: Whether molecule should target scenario_name or all scenarios.
102111
parallel: Whether the scenario(s) should be run in parallel mode.
@@ -115,4 +124,4 @@ def destroy(
115124
if parallel:
116125
util.validate_parallel_cmd_args(command_args)
117126

118-
base.execute_cmdline_scenarios(scenario_name, args, command_args)
127+
base.execute_cmdline_scenarios(scenario_name, args, command_args, excludes=exclude)

0 commit comments

Comments
 (0)