diff --git a/lib/ramble/ramble/language/modifier_language.py b/lib/ramble/ramble/language/modifier_language.py index 9c907f408..ee7b73634 100644 --- a/lib/ramble/ramble/language/modifier_language.py +++ b/lib/ramble/ramble/language/modifier_language.py @@ -112,11 +112,16 @@ def _execute_variable_modification(mod): if mode_name not in mod.variable_modifications: mod.variable_modifications[mode_name] = {} - mod.variable_modifications[mode_name][name] = { - "modification": modification, - "method": method, - "separator": separator, - } + if name not in mod.variable_modifications[mode_name]: + mod.variable_modifications[mode_name][name] = [] + + mod.variable_modifications[mode_name][name].append( + { + "modification": modification, + "method": method, + "separator": separator, + } + ) return _execute_variable_modification diff --git a/lib/ramble/ramble/modifier.py b/lib/ramble/ramble/modifier.py index 1c1a017fc..319f1f3d4 100644 --- a/lib/ramble/ramble/modifier.py +++ b/lib/ramble/ramble/modifier.py @@ -154,26 +154,29 @@ def modded_variables(self, app, extra_vars={}): if self._usage_mode not in self.variable_modifications: return mods - for var, var_mod in self.variable_modifications[self._usage_mode].items(): - if var_mod["method"] in ["append", "prepend"]: - if var in extra_vars: - prev_val = extra_vars[var] - elif var in app.variables: - prev_val = app.variables[var] - else: - prev_val = "" - - if prev_val != "" and prev_val is not None: - sep = var_mod["separator"] - else: - sep = "" - - if var_mod["method"] == "append": - mods[var] = f'{prev_val}{sep}{var_mod["modification"]}' - else: # method == prepend - mods[var] = f'{var_mod["modification"]}{sep}{prev_val}' - else: # method == set - mods[var] = var_mod["modification"] + for var, var_mods in self.variable_modifications[self._usage_mode].items(): + for var_mod in var_mods: + if var_mod["method"] in ["append", "prepend"]: + if var in mods: + prev_val = mods[var] + elif var in extra_vars: + prev_val = extra_vars[var] + elif var in app.variables: + prev_val = app.variables[var] + else: + prev_val = "" + + if prev_val != "" and prev_val is not None: + sep = var_mod["separator"] + else: + sep = "" + + if var_mod["method"] == "append": + mods[var] = f'{prev_val}{sep}{var_mod["modification"]}' + else: # method == prepend + mods[var] = f'{var_mod["modification"]}{sep}{prev_val}' + else: # method == set + mods[var] = var_mod["modification"] return mods diff --git a/lib/ramble/ramble/test/modifier_functionality/mock_repeated_modifications.py b/lib/ramble/ramble/test/modifier_functionality/mock_repeated_modifications.py new file mode 100644 index 000000000..4ce97accd --- /dev/null +++ b/lib/ramble/ramble/test/modifier_functionality/mock_repeated_modifications.py @@ -0,0 +1,49 @@ +# Copyright 2022-2025 The Ramble Authors +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +import os + +from ramble.test.dry_run_helpers import dry_run_config, SCOPES +import ramble.test.modifier_functionality.modifier_helpers as modifier_helpers + +import ramble.workspace +from ramble.main import RambleCommand + +workspace = RambleCommand("workspace") + + +def test_repeated_variable_modifications( + mutable_mock_workspace_path, mutable_applications, mock_modifiers, request +): + workspace_name = request.node.name + + test_modifiers = [ + (SCOPES.experiment, modifier_helpers.named_modifier("repeat-var-mod")), + ] + + with ramble.workspace.create(workspace_name) as ws1: + ws1.write() + + config_path = os.path.join(ws1.config_dir, ramble.workspace.config_file_name) + + dry_run_config("modifiers", test_modifiers, config_path, "gromacs", "water_bare") + + ws1._re_read() + + workspace("concretize", global_args=["-D", ws1.root]) + workspace("setup", "--dry-run", global_args=["-D", ws1.root]) + + rendered_template = os.path.join( + ws1.experiment_dir, "gromacs", "water_bare", "test_exp", "execute_experiment" + ) + assert os.path.exists(rendered_template) + + with open(rendered_template) as f: + data = f.read() + assert "prefix_mpi_command" in data + assert "suffix_mpi_command" in data diff --git a/lib/ramble/ramble/test/modifier_language.py b/lib/ramble/ramble/test/modifier_language.py index 299f7fcac..4e5dc6516 100644 --- a/lib/ramble/ramble/test/modifier_language.py +++ b/lib/ramble/ramble/test/modifier_language.py @@ -93,27 +93,54 @@ def test_variable_modification_directive(mod_class): mod_inst = mod_class("/not/a/path") test_defs.append(add_variable_modification(mod_inst).copy()) + test_defs.append(add_variable_modification(mod_inst, 2).copy()) + test_defs.append(add_variable_modification(mod_inst).copy()) expected_attrs = ["modification", "method"] assert hasattr(mod_inst, "variable_modifications") + mod_count = 0 + for mode_name in mod_inst.variable_modifications: + for var_name in mod_inst.variable_modifications[mode_name]: + for modification in mod_inst.variable_modifications[mode_name][var_name]: + mod_count += 1 + assert mod_count == 9 # Each call to add_variable_modification adds 3 modifications + for test_def in test_defs: var_name = test_def["name"] mode_name = test_def["mode"] assert mode_name in mod_inst.variable_modifications assert var_name in mod_inst.variable_modifications[mode_name] - for attr in expected_attrs: - assert attr in mod_inst.variable_modifications[mode_name][var_name] - assert test_def[attr] == mod_inst.variable_modifications[mode_name][var_name][attr] + found_match = ( + False if len(mod_inst.variable_modifications[mode_name][var_name]) > 0 else True + ) + for modification in mod_inst.variable_modifications[mode_name][var_name]: + match = True + for attr in expected_attrs: + assert attr in modification + if test_def[attr] != modification[attr]: + match = False + if match: + found_match = True + assert found_match for mode_name in test_def["modes"]: + found_match = ( + False if len(mod_inst.variable_modifications[mode_name][var_name]) > 0 else True + ) assert mode_name in mod_inst.variable_modifications assert var_name in mod_inst.variable_modifications[mode_name] - for attr in expected_attrs: - assert attr in mod_inst.variable_modifications[mode_name][var_name] - assert test_def[attr] == mod_inst.variable_modifications[mode_name][var_name][attr] + for modification in mod_inst.variable_modifications[mode_name][var_name]: + match = True + for attr in expected_attrs: + assert attr in modification + if test_def[attr] != modification[attr]: + match = False + if match: + found_match = True + assert found_match @pytest.mark.parametrize("mod_class", mod_types) diff --git a/var/ramble/repos/builtin.mock/modifiers/repeat-var-mod/modifier.py b/var/ramble/repos/builtin.mock/modifiers/repeat-var-mod/modifier.py new file mode 100644 index 000000000..220e755df --- /dev/null +++ b/var/ramble/repos/builtin.mock/modifiers/repeat-var-mod/modifier.py @@ -0,0 +1,34 @@ +# Copyright 2022-2025 The Ramble Authors +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +from ramble.modkit import * # noqa: F403 + + +class RepeatVarMod(BasicModifier): + """Define a test modifier with repeat variable modifications""" + + name = "repeat-var-mod" + + tags("test") + + mode("test", description="This is a test mode") + default_mode("test") + + variable_modification( + "mpi_command", + 'echo "prefix_mpi_command" >> {log_file};', + method="prepend", + modes=["test"], + ) + + variable_modification( + "mpi_command", + 'echo "suffix_mpi_command" >> {log_file};', + method="append", + modes=["test"], + )