Skip to content

Commit 26fb0f0

Browse files
authored
Merge pull request #3370 from vkarak/feat/yaml-config
[feat] Enable YAML configuration files
2 parents 34872a3 + b6db404 commit 26fb0f0

File tree

13 files changed

+432
-28
lines changed

13 files changed

+432
-28
lines changed

docs/config_reference.rst

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@ ReFrame's behavior can be configured through its configuration file, environment
66
An option can be specified via multiple paths (e.g., a configuration file parameter and an environment variable), in which case command-line options precede environment variables, which in turn precede configuration file options.
77
This section provides a complete reference guide of the configuration options of ReFrame that can be set in its configuration file or specified using environment variables.
88

9-
ReFrame's configuration is in JSON syntax.
10-
The full schema describing it can be found in |schemas/config.json|_ file.
11-
The final configuration for ReFrame is validated against this schema.
9+
ReFrame's configuration is a JSON object that is stored in either a Python, JSON or YAML file.
10+
In case of Python configuration, the configuration object must be stored in the special ``site_configuration`` variable:
11+
12+
.. code-block:: python
13+
14+
site_configuration = {
15+
.. # The configuration details.
16+
}
17+
18+
The final configuration is validated against the schema |schemas/config.json|_.
19+
See also :ref:`manpage-configuration` for understanding how ReFrame builds its final configuration.
20+
21+
.. warning::
22+
.. versionchanged:: 4.8
23+
24+
Raw JSON configuration files are deprecated.
1225

1326
The syntax we use to describe the different configuration objects follows the convention: ``OBJECT[.OBJECT]*.PROPERTY``.
1427
Even if a configuration object contains a list of other objects, this is not reflected in the above syntax, as all objects in a certain list are homogeneous.
@@ -83,6 +96,10 @@ It consists of the following properties, which we also call conventionally *conf
8396
2. Shell commands: Any string not prefixed with ``py::`` will be treated as a shell command and will be executed *during auto-detection* to retrieve the hostname.
8497
The standard output of the command will be used.
8598

99+
.. note::
100+
101+
For YAML configuration files the ``py::`` prefixed strings cannot refer to user-defined functions.
102+
86103
If the :option:`--system` option is not passed, ReFrame will try to autodetect the current system trying the methods in this list successively, until one of them succeeds.
87104
The resulting name will be matched against the :attr:`~config.systems.hostnames` patterns of each system and the system that matches first will be used as the current one.
88105

@@ -2288,3 +2305,43 @@ A *device info object* in ReFrame's configuration is used to hold information ab
22882305
:default: ``None``
22892306

22902307
Number of devices of this type inside the system partition.
2308+
2309+
2310+
Dynamic configuration
2311+
=====================
2312+
2313+
One advantage of ReFrame's configuration is that it is programmable, especially if you are using the Python files.
2314+
Since the configuration is loaded as a Python module, you can generate parts of the configuration dynamically.
2315+
2316+
The YAML configuration on the other hand is more static, although not fully.
2317+
Code generation can still be used with the YAML configuration as it is treated as a Jinja2 template, where ReFrame provides the following bindings:
2318+
2319+
- ``getenv(<envvar>)``: Retrieve an environment variable.
2320+
- ``gid``: The real group id of the ReFrame process.
2321+
- ``group``: The group name of the ReFrame process.
2322+
- ``hostname``: The local host's hostname.
2323+
- ``uid``: The real user id of the ReFrame process.
2324+
- ``user``: The user name of the ReFrame process.
2325+
2326+
These are two examples of YAML logging configuration that uses one of those bindings:
2327+
2328+
.. code-block:: yaml
2329+
2330+
logging:
2331+
- handlers:
2332+
- type: file
2333+
name: reframe-{{ hostname }}.log
2334+
level: debug2
2335+
format: "[%(asctime)s.%(msecs)03d] %(levelname)s: %(check_info)s: %(message)s"
2336+
append: false
2337+
2338+
2339+
.. code-block:: yaml
2340+
2341+
logging:
2342+
- handlers:
2343+
- type: file
2344+
name: reframe-{{ getenv("FOO") }}.log
2345+
level: debug2
2346+
format: "[%(asctime)s.%(msecs)03d] %(levelname)s: %(check_info)s: %(message)s"
2347+
append: false

docs/manpage.rst

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2256,16 +2256,27 @@ Whenever an environment variable is associated with a configuration option, its
22562256
================================== ==================
22572257

22582258

2259+
.. _manpage-configuration:
22592260

2260-
Configuration File
2261-
==================
2261+
Configuration
2262+
=============
2263+
2264+
ReFrame's configuration can be stored in one or multiple configuration files.
2265+
Two configuration file types are supported: Python and YAML.
2266+
2267+
.. note::
2268+
2269+
.. versionchanged:: 4.8
2270+
2271+
The JSON configuration files are deprecated.
2272+
2273+
The configuration of ReFrame defines the systems and environments to test as well as parameters controlling the framework's behavior.
22622274

2263-
The configuration file of ReFrame defines the systems and environments to test as well as parameters controlling the framework's behavior.
2275+
To determine its final configuration, ReFrame executes the following steps:
22642276

2265-
ReFrame loads multiple configuration files to determine its final configuration.
2266-
First, it loads unconditionally its builtin configuration which is located in ``${RFM_INSTALL_PREFIX}/reframe/core/settings.py``.
2267-
If the :envvar:`RFM_CONFIG_PATH` environment variable is defined, ReFrame will look for configuration files named either ``settings.py`` or ``settings.json`` (in that order) in every location in the path and will load them.
2268-
Finally, the :option:`--config-file` option is processed and any configuration files specified will also be loaded.
2277+
- First, it unconditionally loads the builtin configuration which is located in ``${RFM_INSTALL_PREFIX}/reframe/core/settings.py``.
2278+
- Second, if the :envvar:`RFM_CONFIG_PATH` environment variable is defined, ReFrame will look for configuration files named either ``settings.py`` or ``settings.yaml`` or ``settings.json`` (in that order) in every location in the path and will load them.
2279+
- Finally, the :option:`--config-file` option is processed and any configuration files specified will also be loaded.
22692280

22702281
For a complete reference of the available configuration options, please refer to the :doc:`reframe.settings(8) <config_reference>` man page.
22712282

docs/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
archspec==0.2.5
22
docutils==0.18.1
3+
jinja2==3.0.3; python_version == '3.6'
4+
jinja2==3.1.4; python_version >= '3.7'
35
jsonschema==3.2.0
6+
PyYAML==6.0.1; python_version < '3.8'
7+
PyYAML==6.0.2; python_version >= '3.8'
48
semver==2.13.0; python_version == '3.6'
59
semver==3.0.4; python_version >= '3.7'
610
Sphinx==5.3.0; python_version < '3.8'

docs/tutorial.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,25 @@ Note also that the system and environment specification in the test run output i
458458
ReFrame has determined that the ``default`` partition and the ``baseline`` environment satisfy the test constraints and thus it has run the test with this partition/environment combination.
459459

460460

461+
YAML configuration
462+
------------------
463+
464+
.. versionadded:: 4.8
465+
466+
Apart from Python, ReFrame's configuration can be specified in a YAML file.
467+
The advantage is a more compact configuration, but it's not fully programmable as is the Python configuration.
468+
Below is the same configuration file presented above, but in YAML:
469+
470+
471+
.. literalinclude:: ../examples/tutorial/config/baseline.yaml
472+
:caption:
473+
:lines: 6-
474+
475+
If you are using multiple configuration files, ReFrame allows you to "mix and match" the configuration file types: some of them can be in Python, others in YAML.
476+
477+
Note that YAML configuration files offer a minimal programmability as they are essentially `Jinja2 <https://jinja.palletsprojects.com/>`__ templates where a few variables are substituted by ReFrame.
478+
Check the configuration reference for more information.
479+
461480
.. _compiling-the-test-code:
462481

463482
Compiling the test code
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2016-2025 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
2+
# ReFrame Project Developers. See the top-level LICENSE file for details.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
systems:
7+
- name: tutorialsys
8+
descr: Example system
9+
hostnames: ['myhost']
10+
partitions:
11+
- name: default
12+
descr: Example partition
13+
scheduler: local
14+
launcher: local
15+
environs: ['baseline']
16+
environments:
17+
- name: baseline
18+
features: ['stream']

reframe/core/config.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
import fnmatch
99
import functools
1010
import importlib
11+
import io
1112
import itertools
1213
import json
1314
import jsonschema
1415
import os
1516
import re
17+
import socket
18+
import yaml
19+
from jinja2.sandbox import SandboxedEnvironment
1620

1721
import reframe
1822
import reframe.core.settings as settings
1923
import reframe.utility as util
2024
import reframe.utility.jsonext as jsonext
2125
import reframe.utility.osext as osext
2226
from reframe.core.environments import normalize_module_list
23-
from reframe.core.exceptions import (ConfigError, ReframeFatalError)
27+
from reframe.core.exceptions import ConfigError, ReframeFatalError
2428
from reframe.core.logging import getlogger
2529
from reframe.utility import ScopedDict
2630

@@ -351,14 +355,37 @@ def load_config_python(self, filename):
351355
def load_config_json(self, filename):
352356
with open(filename) as fp:
353357
try:
354-
config = json.loads(fp.read())
358+
config = json.load(fp)
355359
except json.JSONDecodeError as e:
356360
raise ConfigError(
357361
f"invalid JSON syntax in configuration file '{filename}'"
358362
) from e
359363

360364
self.update_config(config, filename)
361365

366+
def load_config_yaml(self, filename):
367+
bindings = {
368+
'getenv': os.getenv,
369+
'gid': os.getgid(),
370+
'group': osext.osgroup(),
371+
'hostname': socket.gethostname(),
372+
'uid': os.getuid(),
373+
'user': osext.osuser(),
374+
}
375+
376+
with open(filename) as fp:
377+
environment = SandboxedEnvironment()
378+
template = environment.from_string(fp.read())
379+
yaml_src = template.render(**bindings)
380+
try:
381+
config = yaml.safe_load(io.StringIO(yaml_src))
382+
except Exception as err:
383+
raise ConfigError(
384+
f'invalid YAML syntax in configuration file `{filename}`'
385+
) from err
386+
387+
self.update_config(config, filename)
388+
362389
def _setup_autodect_methods(self):
363390
def _py_meth(m):
364391
try:
@@ -429,7 +456,7 @@ def _detect_system(self):
429456
'the `--system` option')
430457

431458
getlogger().debug(f'Retrieved hostname: {hostname!r}')
432-
getlogger().debug(f'Looking for a matching configuration entry')
459+
getlogger().debug('Looking for a matching configuration entry')
433460
for system in self._site_config['systems']:
434461
for patt in system['hostnames']:
435462
if re.match(patt, hostname):
@@ -665,7 +692,13 @@ def load_config(*filenames):
665692
_, ext = os.path.splitext(f)
666693
if ext == '.py':
667694
ret.load_config_python(f)
695+
elif ext == '.yaml' or ext == '.yml':
696+
ret.load_config_yaml(f)
668697
elif ext == '.json':
698+
getlogger().warning(
699+
f'{f}: JSON configuration files are deprecated; '
700+
'please use either a Python or YAML configuration'
701+
)
669702
ret.load_config_json(f)
670703
else:
671704
raise ConfigError(f"unknown configuration file type: '{f}'")

reframe/utility/typecheck.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
A tuple with elements of type :class:`T`.
4747
4848
.. py:data:: Tuple[T1,T2,...,Tn]
49+
:noindex:
4950
5051
A tuple with ``n`` elements, whose types are exactly :class:`T1`,
5152
:class:`T2`, ..., :class:`Tn` in that order.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ filelock==3.12.2; python_version == '3.7'
66
filelock==3.16.1; python_version == '3.8'
77
filelock==3.17.0; python_version > '3.8'
88
importlib_metadata==4.0.1; python_version < '3.8'
9+
jinja2==3.0.3; python_version == '3.6'
10+
jinja2==3.1.4; python_version >= '3.7'
911
jsonschema==3.2.0
1012
lxml==5.2.0; python_version < '3.8' and platform_machine == 'aarch64'
1113
lxml==5.3.0; python_version >= '3.8' or platform_machine != 'aarch64'

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ install_requires =
3434
filelock<=3.16.1; python_version == '3.8'
3535
filelock<=3.12.2; python_version == '3.7'
3636
filelock<=3.4.1; python_version == '3.6'
37+
jinja2==3.0.3; python_version == '3.6'
38+
jinja2
3739
jsonschema
3840
lxml==5.2.0; python_version < '3.8' and platform_machine == 'aarch64'
3941
lxml==5.3.0; python_version >= '3.8' or platform_machine != 'aarch64'
42+
PyYAML==6.0.1; python_version < '3.8'
4043
PyYAML
4144
requests
4245
requests <= 2.27.1; python_version == '3.6'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2016-2025 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
2+
# ReFrame Project Developers. See the top-level LICENSE file for details.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
# Config file for testing the YAML bindings
7+
8+
systems:
9+
- name: testsys
10+
hostnames: ['.*']
11+
partitions:
12+
- name: default
13+
scheduler: local
14+
launcher: local
15+
environs: ['builtin']
16+
extras:
17+
getenv: {{ getenv("_FOO_") }}
18+
gid: {{ gid }}
19+
group: {{ group }}
20+
hostname: {{ hostname }}
21+
uid: {{ uid }}
22+
user: {{ user }}

0 commit comments

Comments
 (0)