2024-02-14 10:02:07 +01:00
|
|
|
"""Test form templates module."""
|
|
|
|
|
|
|
|
import re
|
2024-02-20 15:13:14 +01:00
|
|
|
from pathlib import Path
|
2024-02-14 10:02:07 +01:00
|
|
|
|
2024-02-14 18:45:57 +01:00
|
|
|
from glicid_spawner import form
|
2024-02-20 15:13:14 +01:00
|
|
|
from glicid_spawner.form import options_attrs, options_form, options_from_form
|
2024-02-14 18:45:57 +01:00
|
|
|
from glicid_spawner.micromamba import MicromambaEnv
|
2024-02-20 15:13:14 +01:00
|
|
|
from glicid_spawner.slurm import sinfo_from_file
|
|
|
|
from pytest import fixture
|
2024-02-14 10:02:07 +01:00
|
|
|
|
2024-02-20 15:13:14 +01:00
|
|
|
DATA = Path(__file__).parent / 'data'
|
|
|
|
SINFO = sinfo_from_file(DATA / 'sinfo.txt')
|
2024-02-14 10:02:07 +01:00
|
|
|
|
2024-02-20 15:13:14 +01:00
|
|
|
SLURM_SINGLE_CLUSTER = {'N/A': SINFO.pop('N/A')}
|
|
|
|
SLURM_MULTI_CLUSTER = SINFO
|
|
|
|
|
|
|
|
|
|
|
|
@fixture
|
|
|
|
def mock_python_envs(monkeypatch):
|
|
|
|
"""Mock python environments list."""
|
2024-02-14 10:02:07 +01:00
|
|
|
monkeypatch.setattr(
|
|
|
|
form,
|
|
|
|
'get_envs',
|
|
|
|
lambda username: [
|
2024-02-14 18:45:57 +01:00
|
|
|
MicromambaEnv('USER', 'foo', f'/{username}/envs/foo'),
|
|
|
|
MicromambaEnv('USER', 'bar', f'/{username}/envs/bar'),
|
|
|
|
MicromambaEnv('GLOBAL', 'baz', '/global/envs/baz'),
|
2024-02-14 10:02:07 +01:00
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2024-02-20 15:13:14 +01:00
|
|
|
|
|
|
|
@fixture
|
|
|
|
def mock_cluster(monkeypatch, mock_python_envs):
|
|
|
|
"""Mock multi cluster configuration (default)."""
|
|
|
|
monkeypatch.setattr(form, 'sinfo', lambda _: SLURM_MULTI_CLUSTER)
|
|
|
|
|
|
|
|
|
|
|
|
@fixture
|
|
|
|
def mock_single_cluster(monkeypatch, mock_python_envs):
|
|
|
|
"""Mock multi cluster configuration."""
|
|
|
|
monkeypatch.setattr(form, 'sinfo', lambda _: SLURM_SINGLE_CLUSTER)
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_attrs(mock_cluster):
|
|
|
|
"""Test form options attributes."""
|
|
|
|
options = options_attrs('john-doe')
|
|
|
|
|
|
|
|
assert options['username'] == 'john-doe'
|
|
|
|
|
|
|
|
assert [env.path for env in options['envs']] == [
|
|
|
|
'/john-doe/envs/foo',
|
|
|
|
'/john-doe/envs/bar',
|
|
|
|
'/global/envs/baz',
|
|
|
|
]
|
|
|
|
|
|
|
|
cpu = options['cpus']
|
|
|
|
|
|
|
|
assert cpu[1] == 24
|
|
|
|
assert cpu[24] == 1
|
|
|
|
|
|
|
|
mem = options['mems']
|
|
|
|
|
|
|
|
assert mem[4] == 24
|
|
|
|
assert mem[96] == 1
|
|
|
|
|
|
|
|
gpu = options['gpus']
|
|
|
|
|
|
|
|
assert gpu['None'] == 24
|
|
|
|
assert gpu['A100'] == 1
|
|
|
|
|
|
|
|
# Multi cluster configuration (default)
|
|
|
|
sinfo = options['sinfo']
|
|
|
|
|
|
|
|
assert 'N/A' not in sinfo
|
|
|
|
assert 'nautilus' in sinfo
|
|
|
|
assert 'waves' in sinfo
|
|
|
|
|
|
|
|
node = sinfo['nautilus']['gpu']['gnode1']
|
|
|
|
|
|
|
|
assert node == 'gnode1'
|
|
|
|
assert node.cpu.idle == 92
|
|
|
|
assert node.gpu.name == 'A100'
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_attrs_single_cluster(mock_single_cluster):
|
|
|
|
"""Test form options attributes in single cluster configuration."""
|
|
|
|
options = options_attrs('john-doe')
|
|
|
|
|
|
|
|
# Single cluster configuration
|
|
|
|
sinfo = options['sinfo']
|
|
|
|
|
|
|
|
assert 'N/A' in sinfo
|
|
|
|
assert 'nautilus' not in sinfo
|
|
|
|
assert 'waves' not in sinfo
|
|
|
|
|
|
|
|
node = sinfo['N/A']['Devel']['nazare001']
|
|
|
|
|
|
|
|
assert node == 'nazare001'
|
|
|
|
assert node.cpu.idle == 20
|
|
|
|
assert node.gpu.name == 'None'
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_form_resources(mock_cluster):
|
|
|
|
"""Test options form render."""
|
2024-02-14 10:02:07 +01:00
|
|
|
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
|
|
|
|
|
|
|
# Username
|
|
|
|
assert '<div class="form-control-static">john-doe</div>' in html
|
|
|
|
|
|
|
|
# Python environments
|
|
|
|
assert '<option value="/john-doe/envs/foo">foo (USER)</option>' in html
|
|
|
|
assert '<option value="/john-doe/envs/bar">bar (USER)</option>' in html
|
|
|
|
assert '<option value="/global/envs/baz">baz (GLOBAL)</option>' in html
|
|
|
|
|
|
|
|
# CPU
|
|
|
|
assert (
|
2024-02-20 15:13:14 +01:00
|
|
|
'<input type="radio" name="cpu" id="cpu_1" value="1" data-max-duration="24" checked>'
|
2024-02-14 10:02:07 +01:00
|
|
|
in html
|
|
|
|
)
|
2024-02-20 15:13:14 +01:00
|
|
|
assert '<label for="cpu_1" class="btn btn-default btn-block"> 1 </label>' in html
|
|
|
|
assert '<input type="radio" name="cpu" id="cpu_24" value="24" data-max-duration="1">' in html
|
|
|
|
assert '<label for="cpu_24" class="btn btn-default btn-block"> 24 </label>' in html
|
2024-02-14 10:02:07 +01:00
|
|
|
|
|
|
|
# Memory
|
|
|
|
assert (
|
2024-02-20 15:13:14 +01:00
|
|
|
'<input type="radio" name="mem" id="mem_4" value="4" data-max-duration="24" checked>'
|
2024-02-14 10:02:07 +01:00
|
|
|
in html
|
|
|
|
)
|
2024-02-20 15:13:14 +01:00
|
|
|
assert '<label for="mem_4" class="btn btn-default btn-block"> 4 GB </label>' in html
|
|
|
|
assert '<input type="radio" name="mem" id="mem_96" value="96" data-max-duration="1">' in html
|
|
|
|
assert '<label for="mem_96" class="btn btn-default btn-block"> 96 GB </label>' in html
|
2024-02-14 10:02:07 +01:00
|
|
|
|
|
|
|
# GPU
|
|
|
|
assert (
|
2024-02-20 15:13:14 +01:00
|
|
|
'<input type="radio" name="gpu" id="gpu_None" value="None" data-max-duration="24" checked>'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
assert '<label for="gpu_None" class="btn btn-default btn-block"> None </label>' in html
|
|
|
|
assert (
|
|
|
|
'<input type="radio" name="gpu" id="gpu_A100" value="A100" data-max-duration="1">' in html
|
|
|
|
)
|
|
|
|
assert '<label for="gpu_A100" class="btn btn-default btn-block"> A100 </label>' in html
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_form_slurm(mock_cluster):
|
|
|
|
"""Test options form render."""
|
|
|
|
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
|
|
|
|
|
|
|
# Cluster (multi-cluster by default)
|
|
|
|
assert '<label for="cluster" class="col-sm-3 control-label">Cluster:</label>' in html
|
|
|
|
|
|
|
|
assert '<div class="flex-item-2 slurm-cluster" data-cluster="nautilus">' in html
|
|
|
|
assert '<div class="flex-item-2 slurm-cluster" data-cluster="waves">' in html
|
|
|
|
|
2024-02-20 15:36:40 +01:00
|
|
|
# The 1st cluster is always selected when present…
|
|
|
|
assert (
|
|
|
|
'<input type="radio" name="cluster" id="cluster_nautilus" value="nautilus" checked>' in html
|
|
|
|
)
|
2024-02-20 15:13:14 +01:00
|
|
|
assert (
|
|
|
|
'<label for="cluster_nautilus" class="btn btn-default btn-block"> Nautilus </label>' in html
|
|
|
|
)
|
|
|
|
|
2024-02-20 15:36:40 +01:00
|
|
|
# … not the second one
|
|
|
|
assert '<input type="radio" name="cluster" id="cluster_waves" value="waves" >' in html
|
|
|
|
|
|
|
|
# Partitions
|
2024-02-20 15:13:14 +01:00
|
|
|
assert '<label for="partition" class="col-sm-3 control-label">Partition:</label>' in html
|
|
|
|
|
|
|
|
assert (
|
|
|
|
'<div class="flex-item-4 slurm-partition" '
|
|
|
|
'data-cluster="nautilus" data-partition="gpu" '
|
|
|
|
'data-cpu="96" data-mem="768" data-gpu="A100">' in html
|
|
|
|
)
|
|
|
|
|
|
|
|
assert '<input type="radio" name="partition" id="partition_nautilus_gpu" value="gpu">' in html
|
|
|
|
assert (
|
|
|
|
'<label for="partition_nautilus_gpu" class="btn btn-default btn-block"> Gpu </label>'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
|
|
|
|
# Nodes (hidden by default)
|
|
|
|
assert '<div class="form-group nodes hidden">' in html
|
|
|
|
assert '<label for="node" class="col-sm-3 control-label">Node:</label>' in html
|
|
|
|
|
|
|
|
assert (
|
|
|
|
'<div class="flex-item-4 slurm-node" '
|
|
|
|
'data-cluster="nautilus" data-partition="gpu" data-node="gnode1" '
|
|
|
|
'data-cpu="92" data-mem="768" data-gpu="A100">' in html
|
|
|
|
)
|
|
|
|
|
|
|
|
assert '<input type="radio" name="node" id="node_nautilus_gpu_gnode1" value="gnode1">' in html
|
|
|
|
assert (
|
|
|
|
'<label for="node_nautilus_gpu_gnode1" class="btn btn-default btn-block"> Gnode1 </label>'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_form_slurm_single_cluster(mock_single_cluster):
|
|
|
|
"""Test options form render."""
|
|
|
|
html = re.sub(r'\n\s+', ' ', options_form('john-doe')) # trim line breaks
|
|
|
|
|
|
|
|
# No cluster
|
|
|
|
assert '<label for="cluster" class="col-sm-3 control-label">Cluster:</label>' not in html
|
|
|
|
|
|
|
|
# Partitions (not hidden by default for single-cluster)
|
|
|
|
assert '<div class="form-group partitions">' in html
|
|
|
|
assert '<label for="partition" class="col-sm-3 control-label">Partition:</label>' in html
|
|
|
|
|
|
|
|
assert (
|
|
|
|
'<div class="flex-item-4 slurm-partition" '
|
|
|
|
'data-cluster="N/A" data-partition="GPU-short" '
|
|
|
|
'data-cpu="20" data-mem="184" data-gpu="T4">' in html
|
|
|
|
)
|
|
|
|
|
|
|
|
assert (
|
|
|
|
'<input type="radio" name="partition" id="partition_N/A_GPU-short" value="GPU-short">'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
'<label for="partition_N/A_GPU-short" class="btn btn-default btn-block"> Gpu-short </label>'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
|
|
|
|
# Nodes (hidden by default)
|
|
|
|
assert (
|
|
|
|
'<div class="flex-item-4 slurm-node" '
|
|
|
|
'data-cluster="N/A" data-partition="GPU-short" data-node="budbud001" '
|
|
|
|
'data-cpu="20" data-mem="184" data-gpu="T4">' in html
|
|
|
|
)
|
|
|
|
|
|
|
|
assert (
|
|
|
|
'<input type="radio" name="node" id="node_N/A_GPU-short_budbud001" value="budbud001">'
|
|
|
|
in html
|
|
|
|
)
|
|
|
|
assert (
|
|
|
|
'<label for="node_N/A_GPU-short_budbud001" class="btn btn-default btn-block"> Budbud001 </label>'
|
2024-02-14 10:02:07 +01:00
|
|
|
in html
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def test_options_from_form():
|
|
|
|
"""Test options from form parser."""
|
|
|
|
# No GPU
|
|
|
|
formdata = {
|
2024-02-20 15:13:14 +01:00
|
|
|
'python-env': ['/john-doe/envs/foo'],
|
2024-02-14 10:02:07 +01:00
|
|
|
'cpu': ['1'],
|
2024-02-20 15:13:14 +01:00
|
|
|
'mem': ['4'],
|
|
|
|
'gpu': ['None'],
|
|
|
|
}
|
|
|
|
|
|
|
|
assert options_from_form(formdata) == {
|
|
|
|
'pyenv': '/john-doe/envs/foo',
|
|
|
|
'nprocs': 1,
|
|
|
|
'memory': '4GB',
|
|
|
|
'runtime': '24:00:00',
|
|
|
|
}
|
|
|
|
|
|
|
|
# With GPU (in defaults list)
|
|
|
|
formdata = {
|
|
|
|
'python-env': ['/john-doe/envs/bar'],
|
|
|
|
'cpu': ['2'],
|
|
|
|
'mem': ['16'],
|
|
|
|
'gpu': ['A40'], # -> 2 h
|
|
|
|
'cluster': ['nautilus'],
|
2024-02-14 10:02:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
assert options_from_form(formdata) == {
|
|
|
|
'pyenv': '/john-doe/envs/bar',
|
|
|
|
'nprocs': 2,
|
|
|
|
'memory': '16GB',
|
2024-02-20 15:13:14 +01:00
|
|
|
'runtime': '02:00:00',
|
|
|
|
'gres': 'gpu:a40',
|
|
|
|
'cluster': 'nautilus',
|
2024-02-14 10:02:07 +01:00
|
|
|
}
|
|
|
|
|
2024-02-20 15:13:14 +01:00
|
|
|
# With unknown GPU (default 1h allocation)
|
2024-02-14 10:02:07 +01:00
|
|
|
formdata = {
|
|
|
|
'python-env': ['/global/envs/baz'],
|
2024-02-20 15:13:14 +01:00
|
|
|
'cpu': ['8'],
|
|
|
|
'mem': ['16'],
|
|
|
|
'gpu': ['T4'],
|
|
|
|
'partition': ['GPU-short'],
|
2024-02-14 10:02:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
assert options_from_form(formdata) == {
|
|
|
|
'pyenv': '/global/envs/baz',
|
2024-02-20 15:13:14 +01:00
|
|
|
'nprocs': 8,
|
|
|
|
'memory': '16GB',
|
2024-02-14 10:02:07 +01:00
|
|
|
'runtime': '01:00:00',
|
2024-02-20 15:13:14 +01:00
|
|
|
'gres': 'gpu:t4',
|
|
|
|
'partition': 'GPU-short',
|
|
|
|
}
|
|
|
|
|
|
|
|
# Invalid CPU request (0h allocated)
|
|
|
|
formdata = {
|
|
|
|
'python-env': ['/global/envs/qux'],
|
|
|
|
'cpu': ['128'],
|
|
|
|
'mem': ['4096'],
|
|
|
|
'gpu': ['A100'],
|
|
|
|
'node': ['cribbar001'],
|
|
|
|
}
|
|
|
|
|
|
|
|
assert options_from_form(formdata) == {
|
|
|
|
'pyenv': '/global/envs/qux',
|
|
|
|
'nprocs': 128,
|
|
|
|
'memory': '4096GB',
|
|
|
|
'runtime': '00:00:00',
|
2024-02-14 10:02:07 +01:00
|
|
|
'gres': 'gpu:a100',
|
2024-02-20 15:13:14 +01:00
|
|
|
'node': 'cribbar001',
|
2024-02-14 10:02:07 +01:00
|
|
|
}
|