# Copyright 2022-2026 The Ramble Authors
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
import os
import pytest
import ramble.variants
import ramble.workspace
from ramble.appkit import ExecutableApplication
from ramble.definitions.versions import ObjectVersion
from ramble.error import ObjectValidationError
from ramble.language.language_base import DirectiveError
from ramble.main import RambleCommand
pytestmark = pytest.mark.usefixtures(
"mutable_config",
"mutable_mock_workspace_path",
"mutable_mock_apps_repo",
"mock_modifiers",
"mock_base_applications",
)
config = RambleCommand("config")
workspace = RambleCommand("workspace")
[docs]
def test_only_one_preferred_version_allowed():
app_inst = ExecutableApplication("/not/a/path")
app_inst.preferred_version = ObjectVersion(version_number="1.0", preferred=True)
with pytest.raises(DirectiveError, match="Only one version can be marked preferred."):
app_inst.version("1.1", preferred=True)
[docs]
@pytest.mark.parametrize(
"version,expected_zlib",
[
("2.0a1", {"zlib-greater"}),
("1.0", {"zlib-exact", "zlib-greater", "zlib-range"}),
("0.9", {"zlib-range", "zlib-less"}),
("0.8", {"zlib-less"}),
],
)
def test_application_version_variant_when(workspace_name, version, expected_zlib):
global_args = ["-w", workspace_name]
zlib_versions = {
("zlib-exact", "zlib@1.2.14"),
("zlib-greater", "zlib@1.2.13"),
("zlib-range", "zlib@1.2.12"),
("zlib-less", "zlib@1.2.11"),
}
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
f"versions@{version}",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
for zlib_name, zlib_version in zlib_versions:
if zlib_name in expected_zlib:
assert zlib_version in data
else:
assert zlib_version not in data
[docs]
def test_strict_versions_config(workspace_name, capsys):
global_args = ["-w", workspace_name]
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
"versions@0.1",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
with pytest.raises(ObjectValidationError):
captured = workspace("concretize", global_args=global_args)
assert "You must select from defined versions" in captured
config("add", "config:enable_strict_versions:false", global_args=global_args)
ws._re_read()
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
assert "versions@0.1" in data
[docs]
def test_strict_versions_directive(workspace_name):
"""Test that the strict_versions(False) directive works correctly."""
global_args = ["-w", workspace_name]
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
"versions-strict-disabled@0.1",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
assert "versions-strict-disabled@0.1" in data
[docs]
def test_versions_inherited_from_base_app(workspace_name):
global_args = ["-w", workspace_name]
with ramble.workspace.create(workspace_name) as ws:
# Add a version inherited from base application
workspace(
"manage",
"experiments",
"versions@0.8",
"--wf",
"test_wl_base",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
# Add a version from application
workspace(
"manage",
"experiments",
"versions@1.0",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
assert "versions@0.8" in data
assert "versions@1.0" in data
[docs]
def test_non_pep440_version_number(workspace_name):
global_args = ["-w", workspace_name]
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
"versions-nonstandard@1_01",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
assert "versions-nonstandard@1_01" in data
assert "zlib@1.2.14" in data
output = workspace("info", "-v", global_args=global_args)
assert "Rendered package: versions-nonstandard-1_01" in output
assert "Spec: versions-nonstandard@1_01" in output
assert "versions-nonstandard-1_01 = versions-nonstandard@1_01" in output
assert "zlib-exact = zlib@1.2.14" in output
[docs]
@pytest.mark.parametrize(
"version,expected_zlib",
[
("1_01", {"zlib-exact", "zlib-range", "zlib-less"}),
("1_02", {"zlib-greater", "zlib-range"}),
("2_0a1", {"zlib-greater"}),
("0.8", {"zlib-less"}),
],
)
def test_non_pep440_application_version_variant_when(workspace_name, version, expected_zlib):
global_args = ["-w", workspace_name]
zlib_versions = {
("zlib-exact", "zlib@1.2.14"),
("zlib-greater", "zlib@1.2.13"),
("zlib-range", "zlib@1.2.12"),
("zlib-less", "zlib@1.2.11"),
}
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
f"versions-nonstandard@{version}",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
with open(ws.config_file_path) as f:
data = f.read()
for zlib_name, zlib_version in zlib_versions:
if zlib_name in expected_zlib:
assert zlib_version in data
else:
assert zlib_version not in data
[docs]
def test_version_variable_expansion_info(workspace_name):
global_args = ["-w", workspace_name]
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
"versions@{version}",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-v",
"version=['1.0', '2.0a1']",
"-v",
"custom_var='val_{version}'",
"-p",
"spack",
global_args=global_args,
)
workspace("concretize", global_args=global_args)
output = workspace("info", global_args=global_args)
assert "versions@1.0.test_wl.generated" in output
assert "versions@2.0a1.test_wl.generated" in output
# Verify expansion in the ramble.yaml file
with open(ws.config_file_path) as f:
config_content = f.read()
assert "versions@{version}" in config_content
assert "val_{version}" in config_content
# Verify application_version logic works for expanded experiments
# versions@1.0 should have zlib@1.2.14, versions@2.0a1 should not.
output_v = workspace("info", "-vv", global_args=global_args)
assert "versions@1.0.test_wl.generated" in output_v
assert "versions@2.0a1.test_wl.generated" in output_v
assert "custom_var = 'val_{version}' ==> val_1.0" in output_v
assert "custom_var = 'val_{version}' ==> val_2.0a1" in output_v
assert "zlib-exact = zlib@1.2.14" in output_v
assert "zlib-greater = zlib@1.2.13" in output_v
workspace("setup", "--dry-run", global_args=global_args)
# Verify paths for both experiments
exp1_path = os.path.join(ws.experiment_dir, "versions@1.0", "test_wl", "generated")
exp2_path = os.path.join(ws.experiment_dir, "versions@2.0a1", "test_wl", "generated")
assert os.path.exists(exp1_path)
assert os.path.exists(exp2_path)
[docs]
def test_repeat_modifier_versions_error(workspace_name):
global_args = ["-w", workspace_name]
ramble.workspace.create(workspace_name)
workspace(
"manage",
"experiments",
"versions@0.9",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
global_args=global_args,
)
workspace(
"manage",
"modifiers",
"--add",
"--scope",
"workspace",
"--name",
"versions-mod@1.0",
"--on-executable",
"foo",
global_args=global_args,
)
workspace(
"manage",
"modifiers",
"--add",
"--scope",
"workspace",
"--name",
"versions-mod@2.0",
"--on-executable",
"bar",
global_args=global_args,
)
err_str = (
"Two modifier definitions conflict by having the same name and different version numbers."
)
with pytest.raises(ramble.error.ConflictingModifiersError, match=err_str):
workspace("info", global_args=global_args)
[docs]
def test_multi_modifier_versions(workspace_name):
global_args = ["-w", workspace_name, "--overwrite-inventories"]
with ramble.workspace.create(workspace_name) as ws:
workspace(
"manage",
"experiments",
"versions@0.9",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
global_args=global_args,
)
workspace(
"manage",
"modifiers",
"--add",
"--scope",
"workspace",
"--name",
"versions-mod@1.0",
global_args=global_args,
)
workspace(
"manage",
"modifiers",
"--add",
"--scope",
"workspace",
"--name",
"info@2.0",
global_args=global_args,
)
exec_tpl = os.path.join(ws.config_dir, "execute_experiment.tpl")
with open(exec_tpl, "a+") as f:
f.write("versions-mod version: {modifier::versions-mod::version}\n")
f.write("info version: {modifier::info::version}\n")
workspace("setup", "--dry-run", global_args=global_args)
exec_script = os.path.join(
ws.experiment_dir, "versions@0.9", "test_wl", "generated", "execute_experiment"
)
with open(exec_script) as f:
data = f.read()
assert "versions-mod version: 1.0" in data
assert "info version: 2.0" in data
[docs]
def test_define_version_variables_errors(workspace_name):
global_args = ["-w", workspace_name, "--overwrite-inventories"]
ramble.workspace.create(workspace_name)
workspace(
"manage",
"experiments",
"versions@0.9",
"--wf",
"test_wl",
"-v",
"n_ranks=1",
"-v",
"n_nodes=1",
"-v",
"processes_per_node=1",
"-v",
"application::versions::version=foo",
global_args=global_args,
)
err_str = (
'Keyword "application::versions::version" has been defined, but is reserved by ramble'
)
with pytest.raises(ramble.experiment_set.RambleVariableDefinitionError, match=err_str):
workspace("info", global_args=global_args)