Skip to content

Commit ab904fd

Browse files
authored
Merge pull request #1329 from douglasjacobsen/exec-mod-restrictions
Add usage filters to executable_modifiers
2 parents f83837c + e10f070 commit ab904fd

File tree

13 files changed

+509
-106
lines changed

13 files changed

+509
-106
lines changed

lib/ramble/ramble/language/modifier_language.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def _execute_variable_modification(mod):
131131

132132

133133
@modifier_directive("executable_modifiers")
134-
def executable_modifier(name, when=None, **kwargs):
134+
def executable_modifier(name, usage_filter=None, when=None, **kwargs):
135135
"""Register an executable modifier
136136
137137
Executable modifiers can modify various aspects of non-builtin application
@@ -166,6 +166,9 @@ def write_exec_name(self, executable_name, executable, app_inst=None):
166166
Args:
167167
name (str): Name of executable modifier to use. Should be the name of a
168168
class method.
169+
usage_filter (str): Filters the application of this executable modifier.
170+
Modifiers can register filters to select how to apply this.
171+
Valid default options include: None, "once", "first_mpi", "all_mpi"
169172
when (list | None): List of when conditions this executable modifier should apply in
170173
171174
Each executable modifier needs to return:
@@ -185,6 +188,7 @@ def _executable_modifier(mod):
185188
mod.executable_modifiers[when_set] = {}
186189

187190
mod.executable_modifiers[when_set][name] = {
191+
"usage_filter": usage_filter,
188192
"when": when_list,
189193
}
190194

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright 2022-2025 The Ramble Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
# option. This file may not be copied, modified, or distributed
7+
# except according to those terms.
8+
9+
import os
10+
import re
11+
12+
import pytest
13+
14+
import ramble.workspace
15+
from ramble.error import RambleCommandError
16+
from ramble.main import RambleCommand
17+
18+
workspace = RambleCommand("workspace")
19+
20+
21+
@pytest.mark.parametrize(
22+
"filter_type,expected_count",
23+
[
24+
("none", -1),
25+
("once", 1),
26+
("all_mpi", 6),
27+
("first_mpi", 1),
28+
],
29+
)
30+
def test_executable_modifier_usage_filters(
31+
mutable_mock_workspace_path,
32+
mutable_applications,
33+
mock_modifiers,
34+
workspace_name,
35+
filter_type,
36+
expected_count,
37+
):
38+
39+
global_args = ["-w", workspace_name]
40+
41+
pre_str = f"exec_mod_{filter_type}_pre_applied"
42+
post_str = f"exec_mod_{filter_type}_post_applied"
43+
44+
with ramble.workspace.create(workspace_name) as ws:
45+
ws.write()
46+
47+
workspace(
48+
"manage",
49+
"experiments",
50+
"openfoam",
51+
"--wf",
52+
"motorbike_20m",
53+
"-v",
54+
"n_ranks=1",
55+
"-v",
56+
"n_nodes=1",
57+
"-v",
58+
"openfoam_path=/not/needed",
59+
global_args=global_args,
60+
)
61+
62+
workspace(
63+
"manage",
64+
"modifiers",
65+
"--add",
66+
"--scope",
67+
"workspace",
68+
"--name",
69+
"exec-mod-usage-filters",
70+
global_args=global_args,
71+
)
72+
73+
with open(os.path.join(ws.config_dir, "variants.yaml"), "w+") as f:
74+
f.write(
75+
f"""variants:
76+
usage_filter_type: {filter_type}"""
77+
)
78+
79+
ws._re_read()
80+
81+
workspace("setup", "--dry-run", global_args=global_args)
82+
83+
exec_path = os.path.join(
84+
ws.experiment_dir, "openfoam", "motorbike_20m", "generated", "execute_experiment"
85+
)
86+
87+
assert os.path.isfile(exec_path)
88+
89+
pre_regex = re.compile(pre_str)
90+
post_regex = re.compile(post_str)
91+
92+
with open(exec_path) as f:
93+
pre_count = 0
94+
post_count = 0
95+
96+
for line in f.readlines():
97+
pre_m = pre_regex.search(line)
98+
if pre_m:
99+
pre_count += 1
100+
101+
post_m = post_regex.search(line)
102+
if post_m:
103+
post_count += 1
104+
105+
if expected_count >= 0:
106+
assert pre_count == expected_count
107+
assert post_count == expected_count
108+
else:
109+
assert pre_count > 0
110+
assert post_count > 0
111+
112+
113+
def test_executable_modifier_usage_filters_broken_errors(
114+
mutable_mock_workspace_path,
115+
mutable_applications,
116+
mock_modifiers,
117+
workspace_name,
118+
):
119+
120+
expected_err = (
121+
"When extracting a usage_filter for an executable_modifier "
122+
"on modifier exec-mod-usage-filters the filter __broken__ does not exist"
123+
)
124+
global_args = ["-w", workspace_name]
125+
126+
with ramble.workspace.create(workspace_name) as ws:
127+
ws.write()
128+
129+
workspace(
130+
"manage",
131+
"experiments",
132+
"openfoam",
133+
"--wf",
134+
"motorbike_20m",
135+
"-v",
136+
"n_ranks=1",
137+
"-v",
138+
"n_nodes=1",
139+
"-v",
140+
"openfoam_path=/not/needed",
141+
global_args=global_args,
142+
)
143+
144+
workspace(
145+
"manage",
146+
"modifiers",
147+
"--add",
148+
"--scope",
149+
"workspace",
150+
"--name",
151+
"exec-mod-usage-filters",
152+
global_args=global_args,
153+
)
154+
155+
with open(os.path.join(ws.config_dir, "variants.yaml"), "w+") as f:
156+
f.write(
157+
"""variants:
158+
usage_filter_type: broken"""
159+
)
160+
161+
ws._re_read()
162+
163+
with pytest.raises(RambleCommandError, match=expected_err):
164+
workspace("setup", "--dry-run", global_args=global_args)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright 2022-2025 The Ramble Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
# option. This file may not be copied, modified, or distributed
7+
# except according to those terms.
8+
9+
from ramble.modkit import *
10+
11+
12+
class ExecModUsageFilters(BasicModifier):
13+
"""Define a modifier to test usage filters on executable modifiers"""
14+
15+
name = "exec-mod-usage-filters"
16+
17+
tags("test")
18+
19+
mode("test", description="This is a test mode")
20+
default_mode("test")
21+
22+
modifier_conflict(MODIFIER_CONFLICT.name_mode_executables)
23+
24+
variant(
25+
"usage_filter_type",
26+
default="none",
27+
values=["none", "once", "all_mpi", "first_mpi", "broken"],
28+
description="Control which usage filter to use on exec mods",
29+
)
30+
31+
executable_modifier(
32+
"exec_mod_none", usage_filter=None, when=["usage_filter_type=none"]
33+
)
34+
35+
def exec_mod_none(self, executable_name, executable, app_inst=None):
36+
from ramble.util.executable import CommandExecutable
37+
38+
pre_cmds = [
39+
CommandExecutable(
40+
"exec-mod-none-pre", template=["exec_mod_none_pre_applied"]
41+
)
42+
]
43+
44+
post_cmds = [
45+
CommandExecutable(
46+
"exec-mod-once-post", template=["exec_mod_none_post_applied"]
47+
)
48+
]
49+
50+
return pre_cmds, post_cmds
51+
52+
executable_modifier(
53+
"exec_mod_once", usage_filter="once", when=["usage_filter_type=once"]
54+
)
55+
56+
def exec_mod_once(self, executable_name, executable, app_inst=None):
57+
from ramble.util.executable import CommandExecutable
58+
59+
pre_cmds = [
60+
CommandExecutable(
61+
"exec-mod-once-pre", template=["exec_mod_once_pre_applied"]
62+
)
63+
]
64+
65+
post_cmds = [
66+
CommandExecutable(
67+
"exec-mod-once-post", template=["exec_mod_once_post_applied"]
68+
)
69+
]
70+
71+
return pre_cmds, post_cmds
72+
73+
executable_modifier(
74+
"exec_mod_first_mpi",
75+
usage_filter="first_mpi",
76+
when=["usage_filter_type=first_mpi"],
77+
)
78+
79+
def exec_mod_first_mpi(self, executable_name, executable, app_inst=None):
80+
from ramble.util.executable import CommandExecutable
81+
82+
pre_cmds = [
83+
CommandExecutable(
84+
"exec-mod-first-mpi-pre",
85+
template=["exec_mod_first_mpi_pre_applied"],
86+
)
87+
]
88+
89+
post_cmds = [
90+
CommandExecutable(
91+
"exec-mod-first-mpi-post",
92+
template=["exec_mod_first_mpi_post_applied"],
93+
)
94+
]
95+
96+
return pre_cmds, post_cmds
97+
98+
executable_modifier(
99+
"exec_mod_all_mpi",
100+
usage_filter="all_mpi",
101+
when=["usage_filter_type=all_mpi"],
102+
)
103+
104+
def exec_mod_all_mpi(self, executable_name, executable, app_inst=None):
105+
from ramble.util.executable import CommandExecutable
106+
107+
pre_cmds = [
108+
CommandExecutable(
109+
"exec-mod-all-mpi-pre",
110+
template=["exec_mod_all_mpi_pre_applied"],
111+
)
112+
]
113+
114+
post_cmds = [
115+
CommandExecutable(
116+
"exec-mod-all-mpi-post",
117+
template=["exec_mod_all_mpi_post_applied"],
118+
)
119+
]
120+
121+
return pre_cmds, post_cmds
122+
123+
executable_modifier(
124+
"exec_mod_broken",
125+
usage_filter="__broken__",
126+
when=["usage_filter_type=broken"],
127+
)
128+
129+
def exec_mod_broken(self, executable_name, executable, app_inst=None):
130+
from ramble.util.executable import CommandExecutable
131+
132+
pre_cmds = [
133+
CommandExecutable(
134+
"exec-mod-all-mpi-pre",
135+
template=["exec_mod_all_mpi_pre_applied"],
136+
)
137+
]
138+
139+
post_cmds = [
140+
CommandExecutable(
141+
"exec-mod-all-mpi-post",
142+
template=["exec_mod_all_mpi_post_applied"],
143+
)
144+
]
145+
146+
return pre_cmds, post_cmds

0 commit comments

Comments
 (0)