diff --git a/ci_testing_utilities/harness_unit_tests/Repository_Tests/repository_tests_utility_functions.py b/ci_testing_utilities/harness_unit_tests/Repository_Tests/repository_tests_utility_functions.py
index 2aa4bdbd..c7ead05e 100644
--- a/ci_testing_utilities/harness_unit_tests/Repository_Tests/repository_tests_utility_functions.py
+++ b/ci_testing_utilities/harness_unit_tests/Repository_Tests/repository_tests_utility_functions.py
@@ -3,6 +3,7 @@
import unittest
import os
import shutil
+from pathlib import Path
def get_path_to_sample_directory():
""" Returns the fully qualified path to the directory 'Sample_Directory_For_Repository_Testing'
@@ -26,13 +27,13 @@ def get_path_to_application_directory(tag):
return path_to_dir
def create_application_directory(my_unit_test):
- if os.path.exists(my_unit_test.pathToApplications) :
+ if Path(my_unit_test.pathToApplications).exists():
shutil.rmtree(my_unit_test.pathToApplications)
os.makedirs(my_unit_test.pathToApplications)
return
def creating_root_dir_repo(path_to_repo):
- if os.path.exists(path_to_repo) :
+ if Path(path_to_repo).exists() :
shutil.rmtree(path_to_repo)
os.makedirs(path_to_repo)
return
diff --git a/ci_testing_utilities/harness_unit_tests/Repository_Tests/svn_test_repository.py b/ci_testing_utilities/harness_unit_tests/Repository_Tests/svn_test_repository.py
index a62506e7..953ff971 100644
--- a/ci_testing_utilities/harness_unit_tests/Repository_Tests/svn_test_repository.py
+++ b/ci_testing_utilities/harness_unit_tests/Repository_Tests/svn_test_repository.py
@@ -4,6 +4,7 @@
import unittest
import os
import shutil
+from pathlib import Path
# NCCS Tesst Harness packages
from libraries.repositories import RepositoryFactory
@@ -104,7 +105,7 @@ def get_path_to_test_repository():
return path_head
def creating_root_dir_repo(path_to_repo):
- if os.path.exists(path_to_repo) :
+ if Path(path_to_repo).exists() :
shutil.rmtree(path_to_repo)
os.makedirs(path_to_repo)
return
diff --git a/doc-sphinx/source/conf.py b/doc-sphinx/source/conf.py
index 31cebd0c..885a9905 100644
--- a/doc-sphinx/source/conf.py
+++ b/doc-sphinx/source/conf.py
@@ -36,7 +36,8 @@
'sphinx.ext.doctest',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
- 'sphinx.ext.napoleon'
+ 'sphinx.ext.napoleon',
+ 'sphinx_design'
]
# Add any paths that contain templates here, relative to this directory.
diff --git a/doc-sphinx/source/user_guide/adding_new_machine.rst b/doc-sphinx/source/user_guide/adding_new_machine.rst
index 41876c07..1c1a997e 100644
--- a/doc-sphinx/source/user_guide/adding_new_machine.rst
+++ b/doc-sphinx/source/user_guide/adding_new_machine.rst
@@ -28,7 +28,7 @@ If **RGT_SCHEDULER_TYPE** is set by the user, then the *machine.ini* file will n
[MachineDetails]
# Required variables :
machine_name = frontier
- # options: linux_x86_64 or ibm_power9
+ # options: linux_x86_64 (power9 was recently removed, use linux_x86_64 instead)
machine_type = linux_x86_64
# options: slurm, pbs, lsf
scheduler_type = slurm
diff --git a/doc-sphinx/source/user_guide/adding_new_test.rst b/doc-sphinx/source/user_guide/adding_new_test.rst
index 18ecc4fe..22d0df44 100644
--- a/doc-sphinx/source/user_guide/adding_new_test.rst
+++ b/doc-sphinx/source/user_guide/adding_new_test.rst
@@ -73,68 +73,130 @@ Since these tests are going to share the same source and build script, we are no
Application Test Input
----------------------
-Each test's *Scripts* directory should contain a test input file named *rgt_test_input.ini*.
+Each test's *Scripts* directory should contain a test input file named *rgt_test_input.ini* or *rgt_test_input.yaml*.
The test input file contains information that is used by the OTH to build, submit, and check the results of application tests.
-The test input file follows the Python3 `configparser `_ file format.
-The fields in the ``[DEFAULT]`` section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
-All the fields in the ``[Replacements]`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
-Variables in ``[Replacements]`` cannot be referenced from ``[EnvVars]``.
-The fields in the ``[EnvVars]`` section allow you to set environment variables that all stages of your test will be able to use.
-See :ref:`best-practices` section for recommendations on when to use EnvVars vs Replacements.
+The *ini* file format follows the Python3 `configparser `_ file format, while *yaml* file format is standard YAML.
-.. note::
+.. tab-set::
- Environment variables cannot be used in the definition of other environment variables -- ie, ``foo = $bar`` (See: `Issue 132 `_).
+ .. tab-item:: INI file format
-The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+ The fields in the ``[DEFAULT]`` section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
+ All the fields in the ``[Replacements]`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
+ Variables in ``[Replacements]`` cannot be referenced from ``[EnvVars]``.
+ The fields in the ``[EnvVars]`` section allow you to set environment variables that all stages of your test will be able to use.
+ See :ref:`best-practices` section for recommendations on when to use EnvVars vs Replacements.
-.. code-block:: bash
+ .. note::
- [DEFAULT]
- # This is a comment
- # The DEFAULT section defines variables that can be re-used in Replacements or EnvVars
- # These variables are not automatically used as replacements
- my_custom_variable = abc
+ Environment variables cannot be used in the definition of other environment variables -- ie, ``foo = $bar`` (See: `Issue 132 `_).
- [Replacements]
- #### The following variables are called "built-in", variables the harness knows to look for
- # These are required for every test:
- nodes = 1
- job_name = hello_mpi_c
- walltime = 10
- # %()s is the notation to use the value of a previously-defined variable
- batch_filename = run_%(job_name)s.sh
- build_cmd = ./build_hello_mpi_c.sh
- check_cmd = ./check_hello_mpi_c.sh
- report_cmd = ./report_hello_mpi_c.sh
-
- #### Optional built-in replacements:
- # Useful for controlling relative path inside $BUILD_DIR
- executable_path = hello
- # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
- resubmit = 0
- processes_per_node = 8
- total_processes = 8
- # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
- # Set to 0 (or don't define) for indefinite resubmissions
- max_submissions = 3
-
- # project_id and batch_queue should only be used if a specific partition or account is always required
- #project_id = abc123
- #batch_queue = my_special_partition
-
- #### The following are user-defined and used for Key-Value replacements in the job template
- # NOTE: capital letters in variable names are not supported
- total_processes = 16
- processes_per_node = 16
+ The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+
+ .. code-block:: bash
+
+ [DEFAULT]
+ # This is a comment
+ # The DEFAULT section defines variables that can be re-used in Replacements or EnvVars
+ # These variables are not automatically used as replacements
+ my_custom_variable = abc
+
+ [Replacements]
+ #### The following variables are called "built-in", variables the harness knows to look for
+ # These are required for every test:
+ nodes = 1
+ job_name = hello_mpi_c
+ # %()s is the notation to use the value of a previously-defined variable
+ batch_filename = run_%(job_name)s.sh
+ build_cmd = ./build_hello_mpi_c.sh
+ check_cmd = ./check_hello_mpi_c.sh
+
+ #### Optional built-in replacements:
+ # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
+ resubmit = 0
+ # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
+ # Set to 0 (or don't define) for indefinite resubmissions
+ max_submissions = 3
+
+ # Some build processes will auto-generate a job script, so you can optionally disable the OTH's batch script generation:
+ #use_batch_template = 0
+
+ # project_id and batch_queue should only be used if a specific partition or account is always required
+ #project_id = abc123
+ #batch_queue = my_special_partition
+
+ #### Variables that used to be required and may be useful, but are no longer required:
+ report_cmd = ./report_hello_mpi_c.sh
+ walltime = 10
+ executable_path = hello
+
+ #### The following are user-defined and used for Key-Value replacements in the job template
+ # NOTE: capital letters in variable names are not supported
+ total_processes = 16
+ processes_per_node = 16
- [EnvVars]
- FOO = bar
+ [EnvVars]
+ FOO = bar
+
+ .. note::
+
+ Setting a variable in the Replacements section to ```` pulls in the value set by an environment variable.
+ For example, if you set ``nodes = `` and set *RGT_NODES=4* in your environment prior to running ``runtests.py``, then *__nodes__* will be replaced with 4.
+
+ .. tab-item:: YAML file format
+
+ The *yaml* file format was added to the OTH in 2026, and allows for more powerful templating with Jinja2 template support.
+ There are slight changes to the *yaml* section and variable names relative to the *ini* file format.
+ The fields in the ``variables`` section can be used in the ``replacements`` section of the configuration file by writing a Python format string, as seen below.
+ All the fields in the ``replacements`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
+ Unlike *ini* file format, the *yaml* file does not support ``[EnvVars]``, as using this feature is not good practice.
+ See :ref:`best-practices` section for other test input file recommendations.
+ The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+
+ .. code-block:: bash
+
+ variables:
+ # The variables section defines variables that can be re-used in Replacements or EnvVars
+ # These variables are not automatically used as replacements
+ my_job_name: hello_mpi_c
+
+ replacements:
+ #### The following variables are called "built-in", variables the harness knows to look for
+ # These are required for every test:
+ nodes: 1
+ # Must use quotes (either single or double) when providing Python format strings
+ # Otherwise, YAML thinks you're defining a dictionary
+ job_name: '{my_job_name}'
+ batch_filename: 'run_{my_job_name}.sh'
+ build_cmd: ./build_{my_job_name}.sh
+ check_cmd: ./check_{my_job_name}.sh
+
+ #### Optional built-in replacements:
+ # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
+ resubmit: 0
+ # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
+ # Set to 0 (or don't define) for indefinite resubmissions
+ max_submissions: 3
+
+ # Some build processes will auto-generate a job script, so you can optionally disable the OTH's batch script generation:
+ #use_batch_template: 0
+
+ # project_id and batch_queue should only be used if a specific partition or account is always required
+ #project_id: abc123
+ #batch_queue: my_special_partition
+
+ #### Variables that used to be required and may be useful, but are no longer required:
+ report_cmd: './report_{my_job_name}.sh'
+ walltime: 10
+ executable_path: hello
-.. note::
+ #### The following are user-defined and used for Key-Value replacements in the job template
+ total_processes: 16
+ processes_per_node: 16
- Setting a variable in the Replacements section to ```` pulls in the value set by an environment variable.
- For example, if you set ``nodes = `` and set *RGT_NODES=4* in your environment prior to running ``runtests.py``, then *__nodes__* will be replaced with 4.
+
+ As with *ini*, *yaml* file format supports the ```` option.
+ Unlike *ini*, *yaml* file format does not support using the value of one replacement to define another, it only supports using ``variables`` in the definition of a ``replacement``.
.. _required-application-test-scripts:
@@ -142,12 +204,10 @@ The following is a sample input for the single node test of the *hello_mpi* appl
Required Application Test Scripts
---------------------------------
-The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script, and (4) a reporting script.
+The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script.
These scripts should be placed in the locations described in :ref:`repository-structure`.
-The build, check, and reporting scripts may also be set to Linux commands such as ``/usr/bin/echo``.
-This is useful in cases where a script is not needed.
-For example, a test that relies on standard system-provided tools can set the build script to ``/usr/bin/echo`` to remove the need to have an empty build script.
-If the OTH cannot find the scripts specified by the test input file (*rgt_test_input.ini*), it will fail to launch.
+A test that relies on standard system-provided binaries can set the build script to ``/usr/bin/echo`` to remove the need to have an empty build script.
+If the OTH cannot find the scripts specified by the test input file (*rgt_test_input.[yaml,ini]*), it will fail to launch.
Build Script
^^^^^^^^^^^^
@@ -163,6 +223,8 @@ contain the following:
#!/bin/bash -l
+ set -e # exit on any error
+
module load gcc
module load openmpi
module list
@@ -171,9 +233,9 @@ contain the following:
mpicc hello_mpi.c -o bin/hello
The build command be executed from the directory **$BUILD_DIR**, which is a copy of the contents of *Source/*.
-This means the build script should be written as if it were executed from *Source/*, regardless of where it actually is.
+This means the build script should be written as if it were executed from *Source/*, regardless of where it is located (e.g., *Source/myapp/build_scripts/systems/build_frontier.sh*).
-Likewise, the path to the build script given by *build_cmd* in *rgt_test_input.ini* should be relative to the *Source/* directory.
+Likewise, the path to the build script given by *build_cmd* in *rgt_test_input.[yaml,ini]* should be relative to the *Source/* directory.
.. _job-script-template:
@@ -181,7 +243,7 @@ Job Script Template
^^^^^^^^^^^^^^^^^^^
The OTH will generate the batch job script from the job script template by replacing keywords
-of the form ``__keyword__`` with the values specified in the test input ``[Replacements]`` section.
+of the form ``__keyword__`` with the values specified in the test input file's replacements section.
Additionally, the OTH automatically provides several replacement keywords for the job script to use, described below:
* ``results_dir``: absolute path to the test's *Run_Archive* directory, which is where the job is launched from, and where it typically copies results to
@@ -192,96 +254,184 @@ Additionally, the OTH automatically provides several replacement keywords for th
Generally, these should be used to set environment variables, as shown in the template below.
-The job script template must be named appropriately to match the specific scheduler of the target machine.
-For SLURM systems, use *slurm.template.x* as the name.
-For LSF systems, use *lsf.template.x*.
-An example SLURM template script for the *hello_mpi* application follows:
+The job script template must be named appropriately to match the specific scheduler of the target machine AND *rgt_test_input.[yaml,ini]* file format, as detailed below.
-.. code-block:: bash
++---------------+-------------------+-----------------------+
+| Scheduler | INI test input | YAML test input |
++===============+===================+=======================+
+| Slurm | slurm.template.x | slurm.template.yaml |
++---------------+-------------------+-----------------------+
+| LSF | lsf.template.x | lsf.template.yaml |
++---------------+-------------------+-----------------------+
+| PBS | pbs.template.x | pbs.template.yaml |
++---------------+-------------------+-----------------------+
- #!/bin/bash -l
- #SBATCH -J __job_name__
- #SBATCH -N __nodes__
- #SBATCH -t __walltime__
- #SBATCH -o __job_name__.o%j
+An example Slurm template script for the *hello_mpi* application for both INI+x and YAML+Jinja2 format is provided below.
+In simple cases, there is little functional difference between the two templating formats, but YAML+Jinja2 has far greater power with handling complex test inputs like arrays.
+
+.. tab-set::
+ .. tab-item:: slurm.template.x
+
+ .. code-block:: bash
+
+ #!/bin/bash -l
+ #SBATCH -J __job_name__
+ #SBATCH -N __nodes__
+ #SBATCH -t __walltime__
+ #SBATCH -o __job_name__.o%j
- # Define environment variables needed
- export EXECUTABLE="__executable_path__"
- export SCRIPTS_DIR="__scripts_dir__"
- export WORK_DIR="__working_dir__"
- export RESULTS_DIR="__results_dir__"
- export HARNESS_ID="__harness_id__"
- export BUILD_DIR="__build_dir__"
+ # Define environment variables needed
+ export SCRIPTS_DIR="__scripts_dir__"
+ export WORK_DIR="__working_dir__"
+ export RESULTS_DIR="__results_dir__"
+ export HARNESS_ID="__harness_id__"
+ export BUILD_DIR="__build_dir__"
+
+ export EXECUTABLE="__executable_path__"
- echo "Printing test directory environment variables:"
- env | fgrep RGT_APP_SOURCE_
- env | fgrep RGT_TEST_
- echo
-
- # Placing the environment setup script in a shared location reduces code duplication
- # and ensures you have the same environment in building & running
- source $BUILD_DIR/Common_Scripts/setup_env.sh
+ echo "Printing test directory environment variables:"
+ env | fgrep RGT_APP_SOURCE_
+ env | fgrep RGT_TEST_
+ echo
+
+ # Placing the environment setup script in a shared location reduces code duplication
+ # and ensures you have the same environment in building & running
+ source $BUILD_DIR/Common_Scripts/setup_env.sh
- # Ensure we are in the starting directory
- cd $SCRIPTS_DIR
+ # Ensure we are in the starting directory
+ cd $SCRIPTS_DIR
- # Make the working scratch space directory.
- if [ ! -e $WORK_DIR ]
- then
- mkdir -p $WORK_DIR
- fi
+ # Make the working scratch space directory.
+ [ -d $WORK_DIR ] && mkdir -p $WORK_DIR
- # Change directory to the working directory.
- cd $WORK_DIR
+ # Change directory to the working directory.
+ cd $WORK_DIR
- env &> job.environ
- scontrol show hostnames &> job.nodes
- ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
+ env &> job.environ
+ scontrol show hostnames &> job.nodes
+ ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
- # Run the executable.
- log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
+ # Run the executable.
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
- set -x
- srun -n __total_processes__ -N __nodes__ $BUILD_DIR/bin/$EXECUTABLE
- set +x
+ set -x
+ srun -n __total_processes__ -N __nodes__ $BUILD_DIR/bin/$EXECUTABLE
+ # If wanted, save the exit code & use it to exit the job with
+ exit_code=$?
+ set +x
- log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
- # Ensure we return to the starting directory.
- cd $SCRIPTS_DIR
+ # Ensure we return to the starting directory.
+ cd $SCRIPTS_DIR
- # Copy the output and results back to the $RESULTS_DIR
- # Depending on the size of files in $WORK_DIR, you may want to change this
- cp -rf $WORK_DIR/* $RESULTS_DIR
- cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
+ # Copy the output and results back to the $RESULTS_DIR
+ # Depending on the size of files in $WORK_DIR, you may want to change this
+ cp -rf $WORK_DIR/* $RESULTS_DIR
+ cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
- # Check the final results.
- check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
+ # Check the final results.
+ check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
- # Resubmit if needed:
- # If you always want tests to resubmit if ``.kill_test`` is not present,
- # then remove the conditional around calling ``test_harness_driver.py``.
- case __resubmit__ in
- 0)
- echo "No resubmit";;
- 1)
- test_harness_driver.py -r __max_submissions__ ;;
- esac
-
-Using the job template above, the job will be submitted from the test *Run_Archive/* directory and starts there.
+ # Resubmit if needed:
+ # If you always want tests to resubmit if ``.kill_test`` is not present,
+ # then remove the conditional around calling ``test_harness_driver.py``.
+ case __resubmit__ in
+ 0)
+ echo "No resubmit";;
+ 1)
+ test_harness_driver.py -r __max_submissions__ ;;
+ esac
+
+ exit $exit_code
+
+ .. tab-item:: slurm.template.j2
+
+ .. code-block:: bash
+
+ #!/bin/bash -l
+ #SBATCH -J {{job_name}}
+ #SBATCH -N {{nodes}}
+ #SBATCH -t {{walltime}}
+ #SBATCH -o {{job_name}}.o%j
+
+ # Define environment variables needed
+ export SCRIPTS_DIR="{{scripts_dir}}"
+ export WORK_DIR="{{working_dir}}"
+ export RESULTS_DIR="{{results_dir}}"
+ export HARNESS_ID="{{harness_id}}"
+ export BUILD_DIR="{{build_dir}}"
+
+ export EXECUTABLE="{{executable_path}}"
+
+ echo "Printing test directory environment variables:"
+ env | fgrep RGT_APP_SOURCE_
+ env | fgrep RGT_TEST_
+ echo
+
+ # Placing the environment setup script in a shared location reduces code duplication
+ # and ensures you have the same environment in building & running
+ source $BUILD_DIR/Common_Scripts/setup_env.sh
+
+ # Ensure we are in the starting directory
+ cd $SCRIPTS_DIR
+
+ # Make the working scratch space directory.
+ [ -d $WORK_DIR ] && mkdir -p $WORK_DIR
+
+ # Change directory to the working directory.
+ cd $WORK_DIR
+
+ env &> job.environ
+ scontrol show hostnames &> job.nodes
+ ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
+
+ # Run the executable.
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
+
+ set -x
+ srun -n {{total_processes}} -N {{nodes}} $BUILD_DIR/bin/$EXECUTABLE
+ # If wanted, save the exit code & use it to exit the job with
+ exit_code=$?
+ set +x
+
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
+
+ # Ensure we return to the starting directory.
+ cd $SCRIPTS_DIR
+
+ # Copy the output and results back to the $RESULTS_DIR
+ # Depending on the size of files in $WORK_DIR, you may want to change this
+ cp -rf $WORK_DIR/* $RESULTS_DIR
+ cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
+
+ # Check the final results.
+ check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
+
+ # Resubmit if needed:
+ # If you always want tests to resubmit if ``.kill_test`` is not present,
+ # then remove the conditional around calling ``test_harness_driver.py``.
+ case {{resubmit}} in
+ 0)
+ echo "No resubmit";;
+ 1)
+ test_harness_driver.py -r {{max_submissions}} ;;
+ esac
+
+ exit $exit_code
+
+Using the job template above, the job will be submitted from the test *Run_Archive/* directory and starts from there.
This is **$RESULTS_DIR** in the job template.
-The executable should then be run from **$WORK_DIR** directory, which is a scratch workspace derived from **$RGT_PATH_TO_SSPACE**.
+The executable should then be invoked from **$WORK_DIR** directory, which is a scratch workspace derived from **$RGT_PATH_TO_SSPACE**.
One can access or copy any files relative to the *Scripts/* directory using the **$SCRIPT_DIR** environment variable.
For example, if one stores a *CorrectResults* directory at the same level as *Scripts* and *Run_Archive* for a test case,
-it can be be copied by adding the line
+it can be be copied by adding the following line in the job script:
.. code-block:: bash
cp -a ${SCRIPT_DIR}/../CorrectResults ${WORK_DIR}/
-inside the job script.
-
The environment variable **$EXECUTABLE** is also populated based on ``executable_path`` entry in *rgt_test_input.ini* file.
The executable may still be inside **$BUILD_DIR** from the previous step,
so one would need to either copy it to **$WORK_DIR** or provide the absolute path in the job script such as **$BUILD_DIR/$EXECUTABLE**.
@@ -291,7 +441,7 @@ Check Script
^^^^^^^^^^^^
The check script can be a shell script, Python script, or other executable command.
-This must be an absolute path to a command (ie, ``/usr/bin/echo`` instead of ``echo``).
+The check command is prefixed with **$SCRIPTS_DIR**, so ``check_cmd = check.sh`` is called as ``$SCRIPTS_DIR/check.sh``.
Check scripts are used to verify that application tests ran as expected, and thus use standardized return codes to inform the OTH on the test result.
Checking performance is optional but recommended for most tests.
@@ -325,17 +475,12 @@ contain the following:
Report Script
^^^^^^^^^^^^^
+Optionally, a reporting script may be provided.
Like the check script, the report script can be a shell script, Python script, or other executable command.
-Report scripts are generally used to compute performance metrics from the run.
+Report scripts are generally used to solely gather performance metrics from the run.
The exit code of report scripts is not checked by the OTH.
The report script is launched from **$RESULTS_DIR** and stdout/stderr is captured in **$RESULTS_DIR/output_report.txt**.
-.. note::
-
- In many cases, the check script serves the function of both the check and report script.
- In that event, report scripts often just ``exit 0``.
- An alternative to a no-op bash script, you may use ``/usr/bin/echo`` on most Linux systems.
-
Example Test from the Ground Up
-------------------------------
@@ -353,16 +498,16 @@ At the completion of this section, we will have created a directory structure th
/build.sh
/Common_Scripts/
/setup_env.sh
- /slurm.template.x
+ /slurm.template.j2
/check_hello_world.sh
/hello_world_n0001/Scripts/
- /rgt_test_input.ini
- /slurm.template.x -> ../../Source/Common_Scripts/slurm.template.x
+ /rgt_test_input.yaml
+ /slurm.template.j2 -> ../../Source/Common_Scripts/slurm.template.j2
/check.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/report.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/hello_world_n0002/Scripts/
- /rgt_test_input.ini
- /slurm.template.x -> ../../Source/Common_Scripts/slurm.template.x
+ /rgt_test_input.yaml
+ /slurm.template.j2 -> ../../Source/Common_Scripts/slurm.template.j2
/check.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/report.sh -> ../../Source/Common_Scripts/check_hello_world.sh
@@ -428,57 +573,58 @@ The environment and build scripts will also be the same for both tests, so we ca
' > ./build.sh
Let's give some thought to how we want to construct these tests.
-We'll start by working on the *rgt_test_input.ini* for the single-node *Hello, World!* test.
-Below is a file that can be used for the *rgt_test_input.ini*, with discussion infused as comments.
+We'll start by working on the *rgt_test_input.yaml* for the single-node *Hello, World!* test.
+Below is a file that can be used for the *rgt_test_input.yaml*, with discussion infused as comments.
.. code-block::
- [Replacements]
- job_name = hello_world_n0001
- walltime = 5
- nodes = 1
- # Since nodes is defined, defining the number of MPI ranks per node (processes per node) might be useful, too
- ppn = 2
- # %()s uses the value held by that variable
- batch_filename = run_%(job_name)s.sh
- # executable is in ${BUILD_DIR}/test_src/hello_world
- executable_path = test_src/hello_world
- # build.sh is in Source/build.sh directory
- build_cmd = ./build.sh
- # check.sh is in ${SCRIPTS_DIR}/check.sh
- # I think that providing the total number of expected ranks to the check & report script might be useful in validating
- # This can always be removed later
- check_cmd = ./check.sh $((%(nodes)s*%(ppn)s))
- # report.sh is in ${SCRIPTS_DIR}/check.sh
- report_cmd = ./report.sh $((%(nodes)s*%(ppn)s))
- # Don't allow resubmissions currently
- resubmit = 0
-
- [EnvVars]
- # We don't currently have anything here
-
-Notice that the only lines specific to this test are the *job_name* and *nodes*.
+ variables:
+ nnodes: 1
+ replacements:
+ # when we want to re-use something in the variables section, use a Python format string
+ # Note, it will be .format()'d, so do not use preceeding "f"
+ job_name: 'hello_world_n{nnodes}'
+ walltime: 5
+ nodes: '{nnodes}'
+ # The power of YAML: give a list of processes per node to loop through!
+ ppn_list:
+ - 1
+ - 2
+ - 8
+ batch_filename: 'run_hello_world_n{nnodes}.sh'
+ # executable is in ${BUILD_DIR}/test_src/hello_world
+ executable_path: test_src/hello_world
+ # build.sh is in Source/build.sh directory
+ build_cmd: ./build.sh
+ # check.sh is in ${SCRIPTS_DIR}/check.sh
+ check_cmd: './check.sh {nnodes}'
+ # Don't allow resubmissions currently
+ resubmit: 0
+
+Notice that the only lines specific to this test are up in the ``variables`` section: *nnodes*.
This should help us re-use as much code as possible.
Duplicate code will make tests difficult to maintain in the long run.
Next up is the Slurm template.
+Since we're using YAML input file, we have to use the Jinja2 template.
Moving from 1 to 2 nodes shouldn't change much about the job template, so let's try to develop a generic Slurm job template for *Hello, World!* programs:
.. code-block:: bash
#!/bin/bash
- #SBATCH -J __job_name__
- #SBATCH -N __nodes__
- #SBATCH -t __walltime__
+ #SBATCH -J {{job_name}}
+ #SBATCH -N {{nodes}}
+ #SBATCH -t {{walltime}}
# Define environment variables needed
- export EXECUTABLE="__executable_path__"
- export SCRIPTS_DIR="__scripts_dir__"
- export WORK_DIR="__working_dir__"
- export RESULTS_DIR="__results_dir__"
- export HARNESS_ID="__harness_id__"
- export BUILD_DIR="__build_dir__"
+ export SCRIPTS_DIR="{{scripts_dir}}"
+ export WORK_DIR="{{working_dir}}"
+ export RESULTS_DIR="{{results_dir}}"
+ export HARNESS_ID="{{harness_id}}"
+ export BUILD_DIR="{{build_dir}}"
+
+ export EXECUTABLE="{{executable_path}}"
echo "Printing test directory environment variables:"
env | fgrep RGT_APP_SOURCE_
@@ -512,7 +658,9 @@ Moving from 1 to 2 nodes shouldn't change much about the job template, so let's
# 1. for testing purposes, it's good to ensure that SLURM_NNODES is correct, since users will use that
# 2. if you inadvertently set $RGT_SUBMIT_ARGS, using SLURM_NNODES will adapt to the size of the job
set -x
- srun -N ${SLURM_NNODES} -n $((${SLURM_NNODES}*__ppn__)) --ntasks-per-node=__ppn__ $BUILD_DIR/$EXECUTABLE &> stdout.txt
+ {% for ppn in ppn_list %}
+ srun -N ${SLURM_NNODES} -n $((${SLURM_NNODES}*{{ppn}})) --ntasks-per-node={{ppn}} $BUILD_DIR/$EXECUTABLE |& tee -a stdout.txt
+ {% endfor %}
set +x
log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
@@ -531,11 +679,11 @@ Moving from 1 to 2 nodes shouldn't change much about the job template, so let's
# Resubmit if needed:
# If you always want tests to resubmit if ``.kill_test`` is not present,
# then remove the conditional around calling ``test_harness_driver.py``.
- case __resubmit__ in
+ case {{resubmit}} in
0)
echo "No resubmit";;
1)
- test_harness_driver.py -r __max_submissions__ ;;
+ test_harness_driver.py -r {{max_submissions}} ;;
esac
@@ -547,31 +695,28 @@ Recall that we provided the check script with the total number of tasks to expec
#!/bin/bash
- expected_ranks=$1
+ nnodes=$1
+ # 11 = 1 + 2 + 8
+ expected_ranks=$((nnodes*11))
nranks=$(grep "Hello, World from rank" ${RESULTS_DIR}/stdout.txt | wc -l)
if [ ! "${nranks}" == "${expected_ranks}" ]; then
echo "Found ${nranks}, expected ${expected_ranks}"
exit 1
fi
- echo "Success! Found ${nranks}."
+ echo "Success! Found ${nranks} output lines, which is aligned with the 1, 2, and 8-ppn runs at ${nnodes} nodes."
exit 0
This check script is generic and should be able to be re-used in multiple tests, so let's put it in ``Source/Common_Scripts/check_hello_world.sh``.
-The OTH also wants a report script, but there's not much to report here.
-You can either create a script that immediately exits, or just link to your check script.
-Here, we will just link to the check script.
-
-The Slurm template and check and report scripts are required in the *Scripts* directory, so we use symbolic links to achieve this:
+The Slurm template and check scripts are required in the *Scripts* directory, so we use symbolic links to achieve this:
.. code-block:: bash
# from mpi-tests
cd hello_world_n0001/Scripts
- ln -s ../../Source/Common_Scripts/slurm.template.x .
+ ln -s ../../Source/Common_Scripts/slurm.template.j2 .
ln -s ../../Source/Common_Scripts/check_hello_world.sh ./check.sh
- ln -s ../../Source/Common_Scripts/check_hello_world.sh ./report.sh
To expand to a 2-node *Hello, World!* test, we can just copy the *Scripts* directory from the single-node test, then modify the *rgt_test_input.ini* to specify 2 nodes instead of 1.
@@ -591,10 +736,10 @@ With that in mind, this section presents some of the best practices in test desi
Use a centralized script to set up the environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-During a test run, the environment is independently set up during the build and run stages.
+During a test run, the environment is independently set up during the build and run stages (e.g., the job submission is not done from within the build environment).
If the build script and job script each contain several ``module load`` statements, there is a chance that those can diverge.
To centralize where the environment is set to a single file, place a script containing the ``module`` commands and environment modifications in the build directory,
-and ``source`` that script from the build and job scripts.
+and ``source`` that script from both the build and job scripts.
For the build script, this can be accomplished as simply ``source env.sh``, if the script is in the top level of the Source directory.
For the job script, this can be accomplished by ``source $BUILD_DIR/env.sh``, if the **$BUILD_DIR** environment variable is defined as in the :ref:`job-script-template` section above.
@@ -606,6 +751,7 @@ that you also define a replacement variable in ``[Replacements]`` that is used i
This helps to create a re-usable job script.
If the harness is responsible for defining environment variables that are required for the job to run,
it can be very difficult to understand the resulting job script and to re-run the job script outside of the test harness if needed.
+This is the primary reason why the *yaml* input file format does not support environment variable definition.
The following is recommended within the test input file:
.. code-block:: bash
diff --git a/doc-sphinx/source/user_guide/envvars.rst b/doc-sphinx/source/user_guide/envvars.rst
index a3874e24..7c96ef4d 100644
--- a/doc-sphinx/source/user_guide/envvars.rst
+++ b/doc-sphinx/source/user_guide/envvars.rst
@@ -58,6 +58,7 @@ For example, ``machine_name = `` will fetch the value f
RGT_SUBMIT_ARGS Provide additional flags to use when submitting to the scheduler
RGT_SUBMIT_QUEUE The highest-precedence specification of which scheduler queue/partition to submit to.
RGT_SUBMIT_ACCT The highest-precedence specification of which project ID to submit to.
+ RGT_LSF_SUBMIT_AS_STDIN Controls whether the harness runs ``bsub script.lsf`` or ``bsub < script.lsf``. When set to 0, calls ``bsub script.lsf``.
RGT_TYPE_OF_REPOSITORY Type of repository to access/clone the code. Must be 'git' currently.
RGT_GIT_REPS_BRANCH Branch name to clone a Git repo from. Optional, default behavior is to clone default branch.
diff --git a/doc-sphinx/source/user_guide/extensions.rst b/doc-sphinx/source/user_guide/extensions.rst
index a1c6f1e6..d06b3a92 100644
--- a/doc-sphinx/source/user_guide/extensions.rst
+++ b/doc-sphinx/source/user_guide/extensions.rst
@@ -8,6 +8,8 @@ Extensions
Optional extensions have been developed for use with the OLCF Test Harness (OTH).
Extensions are enabled through environment variables and metadata files placed in the Run_Archive directory of a test launch.
+.. _influxdb_event_logging:
+
Database Event Logging
======================
diff --git a/doc-sphinx/source/user_guide/launching.rst b/doc-sphinx/source/user_guide/launching.rst
index 34dc337e..aca3db61 100644
--- a/doc-sphinx/source/user_guide/launching.rst
+++ b/doc-sphinx/source/user_guide/launching.rst
@@ -29,8 +29,11 @@ Setup the environment:
.. code-block:: bash
export OLCF_HARNESS_DIR=/sw/acceptance/olcf-test-harness
- module use $OLCF_HARNESS_DIR/modulefiles
- module load olcf_harness
+ export PATH=$OLCF_HARNESS_DIR/harness/bin:$OLCF_HARNESS_DIR/harness/utilities:$PATH
+ # Or, use the module (not needed anymore, but does the PATH modification)
+ #module use $OLCF_HARNESS_DIR/modulefiles
+ #module load olcf_harness
+
# Machine name examples: andes, frontier, odo
# Check ${OLCF_HARNESS_DIR}/configs/*.ini to see all available machines
export OLCF_HARNESS_MACHINE=
@@ -50,8 +53,11 @@ Setup the environment:
cd olcf-test-harness
export OLCF_HARNESS_DIR=${PWD}
- module use $OLCF_HARNESS_DIR/modulefiles
- module load olcf_harness
+ export PATH=$PWD/harness/bin:$PWD/harness/utilities:$PATH
+ # Or, use the legacy environment module to do the PATH modification
+ #module use $OLCF_HARNESS_DIR/modulefiles
+ #module load olcf_harness
+
export OLCF_HARNESS_MACHINE=
.. note::
@@ -69,19 +75,20 @@ Launching the OTH
Basic Usage
^^^^^^^^^^^
-Create a directory where you will place input files. No computation will be done here:
+Create a directory where you will place input files, which will instruct the OTH on which tests to launch:
.. code-block:: bash
mkdir summit_testshot
cd summit_testshot
-Prepare an input file of tests (e.g., *rgt.input.summit*).
-In the file, set ``Path_to_tests`` to the location where you would like application source and run files to be kept
-(note that the directory provided must be an existing directory on a file system visible to the current machine).
-Alternatively, set the *RGT_PATH_TO_TESTS* environment variable.
-Next, provide one or more tests to run in the format ``Test = ``.
+Prepare an input file of tests in this directory (e.g., *rgt.input.summit*).
+Provide one or more tests to run in the format ``Test = ``.
In this example for Summit, the application **hello_mpi** is used and we specify two tests: **c_n001** and **c_n002**.
+Optionally, set ``Path_to_tests`` in this file to the location where you would like application source and run files to be kept
+(note that the directory provided must be an existing directory).
+Alternatively, you may set the *RGT_PATH_TO_TESTS* environment variable.
+Either the *RGT_PATH_TO_TESTS* environment variable or ``Path_to_tests`` in the input file must be provided.
.. note::
@@ -92,10 +99,6 @@ In this example for Summit, the application **hello_mpi** is used and we specify
.. code-block:: bash
- ################################################################################
- # Set the path to the top level of the application directory. #
- ################################################################################
-
# Path_to_tests can also replaced by setting the RGT_PATH_TO_TESTS environment variable
Path_to_tests = /some/path/to/my/applications
@@ -122,7 +125,7 @@ Set a scratch area for this specific instance of the harness (a default is set f
export RGT_PATH_TO_TESTS=/some/path/to/my/applications
-The latest version of the harness supports command line tasks as well as input file tasks.
+The harness supports command line tasks as well as input file tasks.
If no tasks are provided in the input file, it will use the command line mode.
To launch via the command line, use a command like the following:
@@ -140,15 +143,13 @@ To launch tasks in the input file instead of the command-line, add lines like th
harness_task check_out_tests
harness_task start_tests
harness_task stop_tests
- harness_task display_status
-
When using the checkout mode, the application source repository will be cloned to the */* directory for all the tests,
but no tests will be run.
If the repository already exists, no action will be taken.
Updating the repo via ``git pull`` or ``git fetch`` should be done outside of the test harness.
-After using the start mode, results of the most recent test run can be found in *///Run_Archive/*.
+After using the start mode, results can be found in *///Run_Archive/*.
Results of the most recent test run can be found in the *///Run_Archive/latest* symbolic link.
.. note::
@@ -169,33 +170,37 @@ The primary OTH driver script, ``runtests.py``, supports the following command-l
.. code-block::
- -h,--help show help message and exit
- -i,--inputfile INPUTFILE Input file name (default: rgt.input)
- -c,--configfile CONFIGFILE Configuration file name (default: ${OLCF_HARNESS_MACHINE}.ini)
- -l,--loglevel LOGLEVEL Logging level (default: NOTSET)
- Options: [NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL]
- -o,--output {screen,logfile} Destination for harness stdout/stderr messages (default: 'screen')
- Options: [screen,logfile]
- 'screen' - print messages to console (default)
- 'logfile' - print messages to log file
- -m,--mode MODE [MODE ...] Specify the mode(s) to run the harness with (default: 'use_input_file')
- Options: [use_input_file,checkout,start,stop,status]
- 'use_input_file' - use tasks defined in the input file
- 'checkout' - checkout application tests listed in input file
- 'start' - start application tests listed in input file
- 'stop' - stop application tests listed in input file
- 'status' - check status of application tests listed in input file
-
- --fireworks Use FireWorks to run harness tasks (beta)
- -sb, --separate-build-stdio Separate output from build into build_out.stderr.txt and build_out.stdout.txt
- --shuffle Shuffle the order of tests before launching
- --reuse-first-build Reuse the first build in a chain of resubmitting tests for all subsequent submissions, per-test.
- --reuse-build-from-id Re-use the build from the specified test ID.
+ -i INPUTFILE, --inputfile INPUTFILE
+ Input file name (default: rgt.input)
+ --shuffle
+ Shuffle the order of tests before launching.
+ -c CONFIGFILE, --configfile CONFIGFILE
+ Configuration file name (default: master.ini)
+ -l {NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL}, --loglevel {NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL}
+ Output logging level (default: WARNING)
+ -o {screen,logfile}, --output {screen,logfile}
+ Destination for harness stdout/stderr messages:
+ 'screen' - print messages to console (default)
+ 'logfile' - print messages to log file
+ -m {task} [{task} ...], --mode {task} [{task} ...]
+ Harness task:
+ 'checkout' - checkout application tests listed in input file
+ 'start' - start application tests listed in input file
+ 'stop' - stop application tests listed in input file
+ 'status' - check status of application tests listed in input file
+ --fireworks
+ Use FireWorks to run harness tasks
+ -sb, --separate-build-stdio
+ Separate output from build into build_out.stderr.txt and build_out.stdout.txt
+ --reuse-first-build
+ If running a test that can resubmit, re-use the first instance of the build for all tests in that chain.
+ --reuse-build-from-id REUSE_BUILD_FROM_ID
+ Re-use the build from the specified test ID.
+ --app-filter APP_FILTER
+ A comma-separated list of regular expressions or strings used to select specific applications from the provided input file.
+ --test-filter TEST_FILTER
+ A comma-separated list of regular expressions or strings used to select specific tests from the provided input file.
-.. note::
-
- The ``--loglevel`` flag currently does not apply to all output from the OTH.
- This issue is tracked by `Issue 130 `_.
.. _runtime_configurable_parameters:
@@ -208,9 +213,9 @@ For example, *git_reps_branch* is a parameter in *$OLCF_HARNESS_MACHINE.ini* tha
The *RGT_GIT_REPS_BRANCH* environment variable can be used to override this value at launch time.
The general precedence of configuration options from lowest to highest is:
-1. *$OLCF_HARNESS_MACHINE.ini*
+1. *$OLCF_HARNESS_MACHINE.ini* (lowest priority)
2. User-set environment variables (ie, *RGT_GIT_REPS_BRANCH*, *RGT_PROJECT_ID*)
-3. *///Scripts/rgt_test_input.ini*
+3. *///Scripts/rgt_test_input.ini* (highest priority)
The specific parameters are defined in :ref:`section_new_test` and :ref:`section_new_machine`.
@@ -239,6 +244,8 @@ There are 4 directories referenced in this section:
- **$RESULTS_DIR** - the directory used to launch the job and store relevant output, equal to **///Run_Archive/**
- **$STATUS_DIR** - the directory used to store harness status files, equal to **///Status/**
+**$RESULTS_DIR** should be considered the best starting point, as it contains symbolic links to the other 3 directories.
+
Build, Submit, and Check Output
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/doc-sphinx/source/user_guide/utilities.rst b/doc-sphinx/source/user_guide/utilities.rst
index aa28f3c8..369efe8b 100644
--- a/doc-sphinx/source/user_guide/utilities.rst
+++ b/doc-sphinx/source/user_guide/utilities.rst
@@ -10,7 +10,7 @@ For example, many of these scripts handle keeping the remote database up-to-date
These scripts are documented below.
rgt_archive_utility.py
-========
+======================
The ``rgt_archive_utility.py`` script allows you to "archive" a test.
What "archive" actually means is it allows you to select when to keep or discard a test's build and work directories, and will copy the test into a single location on the file system, without needing sym-links between the Run_Archive and scratch areas.
@@ -94,7 +94,7 @@ The ``--help`` message for the ``rgt_archive_utility.py`` script is provided bel
update_databases.py
-========
+===================
The ``update_databases.py`` script retrieves all incomplete tests from the remote database (ie, an InfluxDB instance), and tries to determine if that test has completed, but did not log its completion message.
This script has support for the Slurm job scheduler, and will look for the job ID of the given test, to see if it completed.
@@ -154,7 +154,7 @@ This script was written with Cron usage in mind, so the following list of ``--lo
add_comment_to_databases.py
-========
+===========================
The ``add_comment_to_databases.py`` script adds a comment to a specific test instance in the remote database (ie, an InfluxDB instance).
The ``--help`` message for the ``add_comment_to_databases.py`` script is provided below.
@@ -182,7 +182,7 @@ This script requires the same environment variables as the core harness requires
Defaults to most recent event.
report_to_databases.py
-========
+======================
The ``report_to_databases.py`` script enables you to further utilize a remote database to store custom, non-harness metrics.
The ``--help`` message for the ``report_to_databases.py`` script is provided below.
diff --git a/docs/_sources/user_guide/adding_new_machine.rst.txt b/docs/_sources/user_guide/adding_new_machine.rst.txt
index 41876c07..1c1a997e 100644
--- a/docs/_sources/user_guide/adding_new_machine.rst.txt
+++ b/docs/_sources/user_guide/adding_new_machine.rst.txt
@@ -28,7 +28,7 @@ If **RGT_SCHEDULER_TYPE** is set by the user, then the *machine.ini* file will n
[MachineDetails]
# Required variables :
machine_name = frontier
- # options: linux_x86_64 or ibm_power9
+ # options: linux_x86_64 (power9 was recently removed, use linux_x86_64 instead)
machine_type = linux_x86_64
# options: slurm, pbs, lsf
scheduler_type = slurm
diff --git a/docs/_sources/user_guide/adding_new_test.rst.txt b/docs/_sources/user_guide/adding_new_test.rst.txt
index 18ecc4fe..22d0df44 100644
--- a/docs/_sources/user_guide/adding_new_test.rst.txt
+++ b/docs/_sources/user_guide/adding_new_test.rst.txt
@@ -73,68 +73,130 @@ Since these tests are going to share the same source and build script, we are no
Application Test Input
----------------------
-Each test's *Scripts* directory should contain a test input file named *rgt_test_input.ini*.
+Each test's *Scripts* directory should contain a test input file named *rgt_test_input.ini* or *rgt_test_input.yaml*.
The test input file contains information that is used by the OTH to build, submit, and check the results of application tests.
-The test input file follows the Python3 `configparser `_ file format.
-The fields in the ``[DEFAULT]`` section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
-All the fields in the ``[Replacements]`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
-Variables in ``[Replacements]`` cannot be referenced from ``[EnvVars]``.
-The fields in the ``[EnvVars]`` section allow you to set environment variables that all stages of your test will be able to use.
-See :ref:`best-practices` section for recommendations on when to use EnvVars vs Replacements.
+The *ini* file format follows the Python3 `configparser `_ file format, while *yaml* file format is standard YAML.
-.. note::
+.. tab-set::
- Environment variables cannot be used in the definition of other environment variables -- ie, ``foo = $bar`` (See: `Issue 132 `_).
+ .. tab-item:: INI file format
-The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+ The fields in the ``[DEFAULT]`` section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
+ All the fields in the ``[Replacements]`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
+ Variables in ``[Replacements]`` cannot be referenced from ``[EnvVars]``.
+ The fields in the ``[EnvVars]`` section allow you to set environment variables that all stages of your test will be able to use.
+ See :ref:`best-practices` section for recommendations on when to use EnvVars vs Replacements.
-.. code-block:: bash
+ .. note::
- [DEFAULT]
- # This is a comment
- # The DEFAULT section defines variables that can be re-used in Replacements or EnvVars
- # These variables are not automatically used as replacements
- my_custom_variable = abc
+ Environment variables cannot be used in the definition of other environment variables -- ie, ``foo = $bar`` (See: `Issue 132 `_).
- [Replacements]
- #### The following variables are called "built-in", variables the harness knows to look for
- # These are required for every test:
- nodes = 1
- job_name = hello_mpi_c
- walltime = 10
- # %()s is the notation to use the value of a previously-defined variable
- batch_filename = run_%(job_name)s.sh
- build_cmd = ./build_hello_mpi_c.sh
- check_cmd = ./check_hello_mpi_c.sh
- report_cmd = ./report_hello_mpi_c.sh
-
- #### Optional built-in replacements:
- # Useful for controlling relative path inside $BUILD_DIR
- executable_path = hello
- # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
- resubmit = 0
- processes_per_node = 8
- total_processes = 8
- # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
- # Set to 0 (or don't define) for indefinite resubmissions
- max_submissions = 3
-
- # project_id and batch_queue should only be used if a specific partition or account is always required
- #project_id = abc123
- #batch_queue = my_special_partition
-
- #### The following are user-defined and used for Key-Value replacements in the job template
- # NOTE: capital letters in variable names are not supported
- total_processes = 16
- processes_per_node = 16
+ The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+
+ .. code-block:: bash
+
+ [DEFAULT]
+ # This is a comment
+ # The DEFAULT section defines variables that can be re-used in Replacements or EnvVars
+ # These variables are not automatically used as replacements
+ my_custom_variable = abc
+
+ [Replacements]
+ #### The following variables are called "built-in", variables the harness knows to look for
+ # These are required for every test:
+ nodes = 1
+ job_name = hello_mpi_c
+ # %()s is the notation to use the value of a previously-defined variable
+ batch_filename = run_%(job_name)s.sh
+ build_cmd = ./build_hello_mpi_c.sh
+ check_cmd = ./check_hello_mpi_c.sh
+
+ #### Optional built-in replacements:
+ # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
+ resubmit = 0
+ # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
+ # Set to 0 (or don't define) for indefinite resubmissions
+ max_submissions = 3
+
+ # Some build processes will auto-generate a job script, so you can optionally disable the OTH's batch script generation:
+ #use_batch_template = 0
+
+ # project_id and batch_queue should only be used if a specific partition or account is always required
+ #project_id = abc123
+ #batch_queue = my_special_partition
+
+ #### Variables that used to be required and may be useful, but are no longer required:
+ report_cmd = ./report_hello_mpi_c.sh
+ walltime = 10
+ executable_path = hello
+
+ #### The following are user-defined and used for Key-Value replacements in the job template
+ # NOTE: capital letters in variable names are not supported
+ total_processes = 16
+ processes_per_node = 16
- [EnvVars]
- FOO = bar
+ [EnvVars]
+ FOO = bar
+
+ .. note::
+
+ Setting a variable in the Replacements section to ```` pulls in the value set by an environment variable.
+ For example, if you set ``nodes = `` and set *RGT_NODES=4* in your environment prior to running ``runtests.py``, then *__nodes__* will be replaced with 4.
+
+ .. tab-item:: YAML file format
+
+ The *yaml* file format was added to the OTH in 2026, and allows for more powerful templating with Jinja2 template support.
+ There are slight changes to the *yaml* section and variable names relative to the *ini* file format.
+ The fields in the ``variables`` section can be used in the ``replacements`` section of the configuration file by writing a Python format string, as seen below.
+ All the fields in the ``replacements`` section can be used in the job script template and will be replaced when creating the batch script (see :ref:`job-script-template` section below).
+ Unlike *ini* file format, the *yaml* file does not support ``[EnvVars]``, as using this feature is not good practice.
+ See :ref:`best-practices` section for other test input file recommendations.
+ The following is a sample input for the single node test of the *hello_mpi* application mentioned above:
+
+ .. code-block:: bash
+
+ variables:
+ # The variables section defines variables that can be re-used in Replacements or EnvVars
+ # These variables are not automatically used as replacements
+ my_job_name: hello_mpi_c
+
+ replacements:
+ #### The following variables are called "built-in", variables the harness knows to look for
+ # These are required for every test:
+ nodes: 1
+ # Must use quotes (either single or double) when providing Python format strings
+ # Otherwise, YAML thinks you're defining a dictionary
+ job_name: '{my_job_name}'
+ batch_filename: 'run_{my_job_name}.sh'
+ build_cmd: ./build_{my_job_name}.sh
+ check_cmd: ./check_{my_job_name}.sh
+
+ #### Optional built-in replacements:
+ # Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
+ resubmit: 0
+ # Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
+ # Set to 0 (or don't define) for indefinite resubmissions
+ max_submissions: 3
+
+ # Some build processes will auto-generate a job script, so you can optionally disable the OTH's batch script generation:
+ #use_batch_template: 0
+
+ # project_id and batch_queue should only be used if a specific partition or account is always required
+ #project_id: abc123
+ #batch_queue: my_special_partition
+
+ #### Variables that used to be required and may be useful, but are no longer required:
+ report_cmd: './report_{my_job_name}.sh'
+ walltime: 10
+ executable_path: hello
-.. note::
+ #### The following are user-defined and used for Key-Value replacements in the job template
+ total_processes: 16
+ processes_per_node: 16
- Setting a variable in the Replacements section to ```` pulls in the value set by an environment variable.
- For example, if you set ``nodes = `` and set *RGT_NODES=4* in your environment prior to running ``runtests.py``, then *__nodes__* will be replaced with 4.
+
+ As with *ini*, *yaml* file format supports the ```` option.
+ Unlike *ini*, *yaml* file format does not support using the value of one replacement to define another, it only supports using ``variables`` in the definition of a ``replacement``.
.. _required-application-test-scripts:
@@ -142,12 +204,10 @@ The following is a sample input for the single node test of the *hello_mpi* appl
Required Application Test Scripts
---------------------------------
-The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script, and (4) a reporting script.
+The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script.
These scripts should be placed in the locations described in :ref:`repository-structure`.
-The build, check, and reporting scripts may also be set to Linux commands such as ``/usr/bin/echo``.
-This is useful in cases where a script is not needed.
-For example, a test that relies on standard system-provided tools can set the build script to ``/usr/bin/echo`` to remove the need to have an empty build script.
-If the OTH cannot find the scripts specified by the test input file (*rgt_test_input.ini*), it will fail to launch.
+A test that relies on standard system-provided binaries can set the build script to ``/usr/bin/echo`` to remove the need to have an empty build script.
+If the OTH cannot find the scripts specified by the test input file (*rgt_test_input.[yaml,ini]*), it will fail to launch.
Build Script
^^^^^^^^^^^^
@@ -163,6 +223,8 @@ contain the following:
#!/bin/bash -l
+ set -e # exit on any error
+
module load gcc
module load openmpi
module list
@@ -171,9 +233,9 @@ contain the following:
mpicc hello_mpi.c -o bin/hello
The build command be executed from the directory **$BUILD_DIR**, which is a copy of the contents of *Source/*.
-This means the build script should be written as if it were executed from *Source/*, regardless of where it actually is.
+This means the build script should be written as if it were executed from *Source/*, regardless of where it is located (e.g., *Source/myapp/build_scripts/systems/build_frontier.sh*).
-Likewise, the path to the build script given by *build_cmd* in *rgt_test_input.ini* should be relative to the *Source/* directory.
+Likewise, the path to the build script given by *build_cmd* in *rgt_test_input.[yaml,ini]* should be relative to the *Source/* directory.
.. _job-script-template:
@@ -181,7 +243,7 @@ Job Script Template
^^^^^^^^^^^^^^^^^^^
The OTH will generate the batch job script from the job script template by replacing keywords
-of the form ``__keyword__`` with the values specified in the test input ``[Replacements]`` section.
+of the form ``__keyword__`` with the values specified in the test input file's replacements section.
Additionally, the OTH automatically provides several replacement keywords for the job script to use, described below:
* ``results_dir``: absolute path to the test's *Run_Archive* directory, which is where the job is launched from, and where it typically copies results to
@@ -192,96 +254,184 @@ Additionally, the OTH automatically provides several replacement keywords for th
Generally, these should be used to set environment variables, as shown in the template below.
-The job script template must be named appropriately to match the specific scheduler of the target machine.
-For SLURM systems, use *slurm.template.x* as the name.
-For LSF systems, use *lsf.template.x*.
-An example SLURM template script for the *hello_mpi* application follows:
+The job script template must be named appropriately to match the specific scheduler of the target machine AND *rgt_test_input.[yaml,ini]* file format, as detailed below.
-.. code-block:: bash
++---------------+-------------------+-----------------------+
+| Scheduler | INI test input | YAML test input |
++===============+===================+=======================+
+| Slurm | slurm.template.x | slurm.template.yaml |
++---------------+-------------------+-----------------------+
+| LSF | lsf.template.x | lsf.template.yaml |
++---------------+-------------------+-----------------------+
+| PBS | pbs.template.x | pbs.template.yaml |
++---------------+-------------------+-----------------------+
- #!/bin/bash -l
- #SBATCH -J __job_name__
- #SBATCH -N __nodes__
- #SBATCH -t __walltime__
- #SBATCH -o __job_name__.o%j
+An example Slurm template script for the *hello_mpi* application for both INI+x and YAML+Jinja2 format is provided below.
+In simple cases, there is little functional difference between the two templating formats, but YAML+Jinja2 has far greater power with handling complex test inputs like arrays.
+
+.. tab-set::
+ .. tab-item:: slurm.template.x
+
+ .. code-block:: bash
+
+ #!/bin/bash -l
+ #SBATCH -J __job_name__
+ #SBATCH -N __nodes__
+ #SBATCH -t __walltime__
+ #SBATCH -o __job_name__.o%j
- # Define environment variables needed
- export EXECUTABLE="__executable_path__"
- export SCRIPTS_DIR="__scripts_dir__"
- export WORK_DIR="__working_dir__"
- export RESULTS_DIR="__results_dir__"
- export HARNESS_ID="__harness_id__"
- export BUILD_DIR="__build_dir__"
+ # Define environment variables needed
+ export SCRIPTS_DIR="__scripts_dir__"
+ export WORK_DIR="__working_dir__"
+ export RESULTS_DIR="__results_dir__"
+ export HARNESS_ID="__harness_id__"
+ export BUILD_DIR="__build_dir__"
+
+ export EXECUTABLE="__executable_path__"
- echo "Printing test directory environment variables:"
- env | fgrep RGT_APP_SOURCE_
- env | fgrep RGT_TEST_
- echo
-
- # Placing the environment setup script in a shared location reduces code duplication
- # and ensures you have the same environment in building & running
- source $BUILD_DIR/Common_Scripts/setup_env.sh
+ echo "Printing test directory environment variables:"
+ env | fgrep RGT_APP_SOURCE_
+ env | fgrep RGT_TEST_
+ echo
+
+ # Placing the environment setup script in a shared location reduces code duplication
+ # and ensures you have the same environment in building & running
+ source $BUILD_DIR/Common_Scripts/setup_env.sh
- # Ensure we are in the starting directory
- cd $SCRIPTS_DIR
+ # Ensure we are in the starting directory
+ cd $SCRIPTS_DIR
- # Make the working scratch space directory.
- if [ ! -e $WORK_DIR ]
- then
- mkdir -p $WORK_DIR
- fi
+ # Make the working scratch space directory.
+ [ -d $WORK_DIR ] && mkdir -p $WORK_DIR
- # Change directory to the working directory.
- cd $WORK_DIR
+ # Change directory to the working directory.
+ cd $WORK_DIR
- env &> job.environ
- scontrol show hostnames &> job.nodes
- ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
+ env &> job.environ
+ scontrol show hostnames &> job.nodes
+ ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
- # Run the executable.
- log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
+ # Run the executable.
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
- set -x
- srun -n __total_processes__ -N __nodes__ $BUILD_DIR/bin/$EXECUTABLE
- set +x
+ set -x
+ srun -n __total_processes__ -N __nodes__ $BUILD_DIR/bin/$EXECUTABLE
+ # If wanted, save the exit code & use it to exit the job with
+ exit_code=$?
+ set +x
- log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
- # Ensure we return to the starting directory.
- cd $SCRIPTS_DIR
+ # Ensure we return to the starting directory.
+ cd $SCRIPTS_DIR
- # Copy the output and results back to the $RESULTS_DIR
- # Depending on the size of files in $WORK_DIR, you may want to change this
- cp -rf $WORK_DIR/* $RESULTS_DIR
- cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
+ # Copy the output and results back to the $RESULTS_DIR
+ # Depending on the size of files in $WORK_DIR, you may want to change this
+ cp -rf $WORK_DIR/* $RESULTS_DIR
+ cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
- # Check the final results.
- check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
+ # Check the final results.
+ check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
- # Resubmit if needed:
- # If you always want tests to resubmit if ``.kill_test`` is not present,
- # then remove the conditional around calling ``test_harness_driver.py``.
- case __resubmit__ in
- 0)
- echo "No resubmit";;
- 1)
- test_harness_driver.py -r __max_submissions__ ;;
- esac
-
-Using the job template above, the job will be submitted from the test *Run_Archive/* directory and starts there.
+ # Resubmit if needed:
+ # If you always want tests to resubmit if ``.kill_test`` is not present,
+ # then remove the conditional around calling ``test_harness_driver.py``.
+ case __resubmit__ in
+ 0)
+ echo "No resubmit";;
+ 1)
+ test_harness_driver.py -r __max_submissions__ ;;
+ esac
+
+ exit $exit_code
+
+ .. tab-item:: slurm.template.j2
+
+ .. code-block:: bash
+
+ #!/bin/bash -l
+ #SBATCH -J {{job_name}}
+ #SBATCH -N {{nodes}}
+ #SBATCH -t {{walltime}}
+ #SBATCH -o {{job_name}}.o%j
+
+ # Define environment variables needed
+ export SCRIPTS_DIR="{{scripts_dir}}"
+ export WORK_DIR="{{working_dir}}"
+ export RESULTS_DIR="{{results_dir}}"
+ export HARNESS_ID="{{harness_id}}"
+ export BUILD_DIR="{{build_dir}}"
+
+ export EXECUTABLE="{{executable_path}}"
+
+ echo "Printing test directory environment variables:"
+ env | fgrep RGT_APP_SOURCE_
+ env | fgrep RGT_TEST_
+ echo
+
+ # Placing the environment setup script in a shared location reduces code duplication
+ # and ensures you have the same environment in building & running
+ source $BUILD_DIR/Common_Scripts/setup_env.sh
+
+ # Ensure we are in the starting directory
+ cd $SCRIPTS_DIR
+
+ # Make the working scratch space directory.
+ [ -d $WORK_DIR ] && mkdir -p $WORK_DIR
+
+ # Change directory to the working directory.
+ cd $WORK_DIR
+
+ env &> job.environ
+ scontrol show hostnames &> job.nodes
+ ldd $BUILD_DIR/bin/$EXECUTABLE &> ldd.log
+
+ # Run the executable.
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode start
+
+ set -x
+ srun -n {{total_processes}} -N {{nodes}} $BUILD_DIR/bin/$EXECUTABLE
+ # If wanted, save the exit code & use it to exit the job with
+ exit_code=$?
+ set +x
+
+ log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
+
+ # Ensure we return to the starting directory.
+ cd $SCRIPTS_DIR
+
+ # Copy the output and results back to the $RESULTS_DIR
+ # Depending on the size of files in $WORK_DIR, you may want to change this
+ cp -rf $WORK_DIR/* $RESULTS_DIR
+ cp $BUILD_DIR/output_build*.txt $RESULTS_DIR
+
+ # Check the final results.
+ check_executable_driver.py -p $RESULTS_DIR -i $HARNESS_ID
+
+ # Resubmit if needed:
+ # If you always want tests to resubmit if ``.kill_test`` is not present,
+ # then remove the conditional around calling ``test_harness_driver.py``.
+ case {{resubmit}} in
+ 0)
+ echo "No resubmit";;
+ 1)
+ test_harness_driver.py -r {{max_submissions}} ;;
+ esac
+
+ exit $exit_code
+
+Using the job template above, the job will be submitted from the test *Run_Archive/* directory and starts from there.
This is **$RESULTS_DIR** in the job template.
-The executable should then be run from **$WORK_DIR** directory, which is a scratch workspace derived from **$RGT_PATH_TO_SSPACE**.
+The executable should then be invoked from **$WORK_DIR** directory, which is a scratch workspace derived from **$RGT_PATH_TO_SSPACE**.
One can access or copy any files relative to the *Scripts/* directory using the **$SCRIPT_DIR** environment variable.
For example, if one stores a *CorrectResults* directory at the same level as *Scripts* and *Run_Archive* for a test case,
-it can be be copied by adding the line
+it can be be copied by adding the following line in the job script:
.. code-block:: bash
cp -a ${SCRIPT_DIR}/../CorrectResults ${WORK_DIR}/
-inside the job script.
-
The environment variable **$EXECUTABLE** is also populated based on ``executable_path`` entry in *rgt_test_input.ini* file.
The executable may still be inside **$BUILD_DIR** from the previous step,
so one would need to either copy it to **$WORK_DIR** or provide the absolute path in the job script such as **$BUILD_DIR/$EXECUTABLE**.
@@ -291,7 +441,7 @@ Check Script
^^^^^^^^^^^^
The check script can be a shell script, Python script, or other executable command.
-This must be an absolute path to a command (ie, ``/usr/bin/echo`` instead of ``echo``).
+The check command is prefixed with **$SCRIPTS_DIR**, so ``check_cmd = check.sh`` is called as ``$SCRIPTS_DIR/check.sh``.
Check scripts are used to verify that application tests ran as expected, and thus use standardized return codes to inform the OTH on the test result.
Checking performance is optional but recommended for most tests.
@@ -325,17 +475,12 @@ contain the following:
Report Script
^^^^^^^^^^^^^
+Optionally, a reporting script may be provided.
Like the check script, the report script can be a shell script, Python script, or other executable command.
-Report scripts are generally used to compute performance metrics from the run.
+Report scripts are generally used to solely gather performance metrics from the run.
The exit code of report scripts is not checked by the OTH.
The report script is launched from **$RESULTS_DIR** and stdout/stderr is captured in **$RESULTS_DIR/output_report.txt**.
-.. note::
-
- In many cases, the check script serves the function of both the check and report script.
- In that event, report scripts often just ``exit 0``.
- An alternative to a no-op bash script, you may use ``/usr/bin/echo`` on most Linux systems.
-
Example Test from the Ground Up
-------------------------------
@@ -353,16 +498,16 @@ At the completion of this section, we will have created a directory structure th
/build.sh
/Common_Scripts/
/setup_env.sh
- /slurm.template.x
+ /slurm.template.j2
/check_hello_world.sh
/hello_world_n0001/Scripts/
- /rgt_test_input.ini
- /slurm.template.x -> ../../Source/Common_Scripts/slurm.template.x
+ /rgt_test_input.yaml
+ /slurm.template.j2 -> ../../Source/Common_Scripts/slurm.template.j2
/check.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/report.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/hello_world_n0002/Scripts/
- /rgt_test_input.ini
- /slurm.template.x -> ../../Source/Common_Scripts/slurm.template.x
+ /rgt_test_input.yaml
+ /slurm.template.j2 -> ../../Source/Common_Scripts/slurm.template.j2
/check.sh -> ../../Source/Common_Scripts/check_hello_world.sh
/report.sh -> ../../Source/Common_Scripts/check_hello_world.sh
@@ -428,57 +573,58 @@ The environment and build scripts will also be the same for both tests, so we ca
' > ./build.sh
Let's give some thought to how we want to construct these tests.
-We'll start by working on the *rgt_test_input.ini* for the single-node *Hello, World!* test.
-Below is a file that can be used for the *rgt_test_input.ini*, with discussion infused as comments.
+We'll start by working on the *rgt_test_input.yaml* for the single-node *Hello, World!* test.
+Below is a file that can be used for the *rgt_test_input.yaml*, with discussion infused as comments.
.. code-block::
- [Replacements]
- job_name = hello_world_n0001
- walltime = 5
- nodes = 1
- # Since nodes is defined, defining the number of MPI ranks per node (processes per node) might be useful, too
- ppn = 2
- # %()s uses the value held by that variable
- batch_filename = run_%(job_name)s.sh
- # executable is in ${BUILD_DIR}/test_src/hello_world
- executable_path = test_src/hello_world
- # build.sh is in Source/build.sh directory
- build_cmd = ./build.sh
- # check.sh is in ${SCRIPTS_DIR}/check.sh
- # I think that providing the total number of expected ranks to the check & report script might be useful in validating
- # This can always be removed later
- check_cmd = ./check.sh $((%(nodes)s*%(ppn)s))
- # report.sh is in ${SCRIPTS_DIR}/check.sh
- report_cmd = ./report.sh $((%(nodes)s*%(ppn)s))
- # Don't allow resubmissions currently
- resubmit = 0
-
- [EnvVars]
- # We don't currently have anything here
-
-Notice that the only lines specific to this test are the *job_name* and *nodes*.
+ variables:
+ nnodes: 1
+ replacements:
+ # when we want to re-use something in the variables section, use a Python format string
+ # Note, it will be .format()'d, so do not use preceeding "f"
+ job_name: 'hello_world_n{nnodes}'
+ walltime: 5
+ nodes: '{nnodes}'
+ # The power of YAML: give a list of processes per node to loop through!
+ ppn_list:
+ - 1
+ - 2
+ - 8
+ batch_filename: 'run_hello_world_n{nnodes}.sh'
+ # executable is in ${BUILD_DIR}/test_src/hello_world
+ executable_path: test_src/hello_world
+ # build.sh is in Source/build.sh directory
+ build_cmd: ./build.sh
+ # check.sh is in ${SCRIPTS_DIR}/check.sh
+ check_cmd: './check.sh {nnodes}'
+ # Don't allow resubmissions currently
+ resubmit: 0
+
+Notice that the only lines specific to this test are up in the ``variables`` section: *nnodes*.
This should help us re-use as much code as possible.
Duplicate code will make tests difficult to maintain in the long run.
Next up is the Slurm template.
+Since we're using YAML input file, we have to use the Jinja2 template.
Moving from 1 to 2 nodes shouldn't change much about the job template, so let's try to develop a generic Slurm job template for *Hello, World!* programs:
.. code-block:: bash
#!/bin/bash
- #SBATCH -J __job_name__
- #SBATCH -N __nodes__
- #SBATCH -t __walltime__
+ #SBATCH -J {{job_name}}
+ #SBATCH -N {{nodes}}
+ #SBATCH -t {{walltime}}
# Define environment variables needed
- export EXECUTABLE="__executable_path__"
- export SCRIPTS_DIR="__scripts_dir__"
- export WORK_DIR="__working_dir__"
- export RESULTS_DIR="__results_dir__"
- export HARNESS_ID="__harness_id__"
- export BUILD_DIR="__build_dir__"
+ export SCRIPTS_DIR="{{scripts_dir}}"
+ export WORK_DIR="{{working_dir}}"
+ export RESULTS_DIR="{{results_dir}}"
+ export HARNESS_ID="{{harness_id}}"
+ export BUILD_DIR="{{build_dir}}"
+
+ export EXECUTABLE="{{executable_path}}"
echo "Printing test directory environment variables:"
env | fgrep RGT_APP_SOURCE_
@@ -512,7 +658,9 @@ Moving from 1 to 2 nodes shouldn't change much about the job template, so let's
# 1. for testing purposes, it's good to ensure that SLURM_NNODES is correct, since users will use that
# 2. if you inadvertently set $RGT_SUBMIT_ARGS, using SLURM_NNODES will adapt to the size of the job
set -x
- srun -N ${SLURM_NNODES} -n $((${SLURM_NNODES}*__ppn__)) --ntasks-per-node=__ppn__ $BUILD_DIR/$EXECUTABLE &> stdout.txt
+ {% for ppn in ppn_list %}
+ srun -N ${SLURM_NNODES} -n $((${SLURM_NNODES}*{{ppn}})) --ntasks-per-node={{ppn}} $BUILD_DIR/$EXECUTABLE |& tee -a stdout.txt
+ {% endfor %}
set +x
log_binary_execution_time.py --scriptsdir $SCRIPTS_DIR --uniqueid $HARNESS_ID --mode final
@@ -531,11 +679,11 @@ Moving from 1 to 2 nodes shouldn't change much about the job template, so let's
# Resubmit if needed:
# If you always want tests to resubmit if ``.kill_test`` is not present,
# then remove the conditional around calling ``test_harness_driver.py``.
- case __resubmit__ in
+ case {{resubmit}} in
0)
echo "No resubmit";;
1)
- test_harness_driver.py -r __max_submissions__ ;;
+ test_harness_driver.py -r {{max_submissions}} ;;
esac
@@ -547,31 +695,28 @@ Recall that we provided the check script with the total number of tasks to expec
#!/bin/bash
- expected_ranks=$1
+ nnodes=$1
+ # 11 = 1 + 2 + 8
+ expected_ranks=$((nnodes*11))
nranks=$(grep "Hello, World from rank" ${RESULTS_DIR}/stdout.txt | wc -l)
if [ ! "${nranks}" == "${expected_ranks}" ]; then
echo "Found ${nranks}, expected ${expected_ranks}"
exit 1
fi
- echo "Success! Found ${nranks}."
+ echo "Success! Found ${nranks} output lines, which is aligned with the 1, 2, and 8-ppn runs at ${nnodes} nodes."
exit 0
This check script is generic and should be able to be re-used in multiple tests, so let's put it in ``Source/Common_Scripts/check_hello_world.sh``.
-The OTH also wants a report script, but there's not much to report here.
-You can either create a script that immediately exits, or just link to your check script.
-Here, we will just link to the check script.
-
-The Slurm template and check and report scripts are required in the *Scripts* directory, so we use symbolic links to achieve this:
+The Slurm template and check scripts are required in the *Scripts* directory, so we use symbolic links to achieve this:
.. code-block:: bash
# from mpi-tests
cd hello_world_n0001/Scripts
- ln -s ../../Source/Common_Scripts/slurm.template.x .
+ ln -s ../../Source/Common_Scripts/slurm.template.j2 .
ln -s ../../Source/Common_Scripts/check_hello_world.sh ./check.sh
- ln -s ../../Source/Common_Scripts/check_hello_world.sh ./report.sh
To expand to a 2-node *Hello, World!* test, we can just copy the *Scripts* directory from the single-node test, then modify the *rgt_test_input.ini* to specify 2 nodes instead of 1.
@@ -591,10 +736,10 @@ With that in mind, this section presents some of the best practices in test desi
Use a centralized script to set up the environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-During a test run, the environment is independently set up during the build and run stages.
+During a test run, the environment is independently set up during the build and run stages (e.g., the job submission is not done from within the build environment).
If the build script and job script each contain several ``module load`` statements, there is a chance that those can diverge.
To centralize where the environment is set to a single file, place a script containing the ``module`` commands and environment modifications in the build directory,
-and ``source`` that script from the build and job scripts.
+and ``source`` that script from both the build and job scripts.
For the build script, this can be accomplished as simply ``source env.sh``, if the script is in the top level of the Source directory.
For the job script, this can be accomplished by ``source $BUILD_DIR/env.sh``, if the **$BUILD_DIR** environment variable is defined as in the :ref:`job-script-template` section above.
@@ -606,6 +751,7 @@ that you also define a replacement variable in ``[Replacements]`` that is used i
This helps to create a re-usable job script.
If the harness is responsible for defining environment variables that are required for the job to run,
it can be very difficult to understand the resulting job script and to re-run the job script outside of the test harness if needed.
+This is the primary reason why the *yaml* input file format does not support environment variable definition.
The following is recommended within the test input file:
.. code-block:: bash
diff --git a/docs/_sources/user_guide/envvars.rst.txt b/docs/_sources/user_guide/envvars.rst.txt
index a3874e24..7c96ef4d 100644
--- a/docs/_sources/user_guide/envvars.rst.txt
+++ b/docs/_sources/user_guide/envvars.rst.txt
@@ -58,6 +58,7 @@ For example, ``machine_name = `` will fetch the value f
RGT_SUBMIT_ARGS Provide additional flags to use when submitting to the scheduler
RGT_SUBMIT_QUEUE The highest-precedence specification of which scheduler queue/partition to submit to.
RGT_SUBMIT_ACCT The highest-precedence specification of which project ID to submit to.
+ RGT_LSF_SUBMIT_AS_STDIN Controls whether the harness runs ``bsub script.lsf`` or ``bsub < script.lsf``. When set to 0, calls ``bsub script.lsf``.
RGT_TYPE_OF_REPOSITORY Type of repository to access/clone the code. Must be 'git' currently.
RGT_GIT_REPS_BRANCH Branch name to clone a Git repo from. Optional, default behavior is to clone default branch.
diff --git a/docs/_sources/user_guide/extensions.rst.txt b/docs/_sources/user_guide/extensions.rst.txt
index a1c6f1e6..d06b3a92 100644
--- a/docs/_sources/user_guide/extensions.rst.txt
+++ b/docs/_sources/user_guide/extensions.rst.txt
@@ -8,6 +8,8 @@ Extensions
Optional extensions have been developed for use with the OLCF Test Harness (OTH).
Extensions are enabled through environment variables and metadata files placed in the Run_Archive directory of a test launch.
+.. _influxdb_event_logging:
+
Database Event Logging
======================
diff --git a/docs/_sources/user_guide/launching.rst.txt b/docs/_sources/user_guide/launching.rst.txt
index 34dc337e..aca3db61 100644
--- a/docs/_sources/user_guide/launching.rst.txt
+++ b/docs/_sources/user_guide/launching.rst.txt
@@ -29,8 +29,11 @@ Setup the environment:
.. code-block:: bash
export OLCF_HARNESS_DIR=/sw/acceptance/olcf-test-harness
- module use $OLCF_HARNESS_DIR/modulefiles
- module load olcf_harness
+ export PATH=$OLCF_HARNESS_DIR/harness/bin:$OLCF_HARNESS_DIR/harness/utilities:$PATH
+ # Or, use the module (not needed anymore, but does the PATH modification)
+ #module use $OLCF_HARNESS_DIR/modulefiles
+ #module load olcf_harness
+
# Machine name examples: andes, frontier, odo
# Check ${OLCF_HARNESS_DIR}/configs/*.ini to see all available machines
export OLCF_HARNESS_MACHINE=
@@ -50,8 +53,11 @@ Setup the environment:
cd olcf-test-harness
export OLCF_HARNESS_DIR=${PWD}
- module use $OLCF_HARNESS_DIR/modulefiles
- module load olcf_harness
+ export PATH=$PWD/harness/bin:$PWD/harness/utilities:$PATH
+ # Or, use the legacy environment module to do the PATH modification
+ #module use $OLCF_HARNESS_DIR/modulefiles
+ #module load olcf_harness
+
export OLCF_HARNESS_MACHINE=
.. note::
@@ -69,19 +75,20 @@ Launching the OTH
Basic Usage
^^^^^^^^^^^
-Create a directory where you will place input files. No computation will be done here:
+Create a directory where you will place input files, which will instruct the OTH on which tests to launch:
.. code-block:: bash
mkdir summit_testshot
cd summit_testshot
-Prepare an input file of tests (e.g., *rgt.input.summit*).
-In the file, set ``Path_to_tests`` to the location where you would like application source and run files to be kept
-(note that the directory provided must be an existing directory on a file system visible to the current machine).
-Alternatively, set the *RGT_PATH_TO_TESTS* environment variable.
-Next, provide one or more tests to run in the format ``Test = ``.
+Prepare an input file of tests in this directory (e.g., *rgt.input.summit*).
+Provide one or more tests to run in the format ``Test = ``.
In this example for Summit, the application **hello_mpi** is used and we specify two tests: **c_n001** and **c_n002**.
+Optionally, set ``Path_to_tests`` in this file to the location where you would like application source and run files to be kept
+(note that the directory provided must be an existing directory).
+Alternatively, you may set the *RGT_PATH_TO_TESTS* environment variable.
+Either the *RGT_PATH_TO_TESTS* environment variable or ``Path_to_tests`` in the input file must be provided.
.. note::
@@ -92,10 +99,6 @@ In this example for Summit, the application **hello_mpi** is used and we specify
.. code-block:: bash
- ################################################################################
- # Set the path to the top level of the application directory. #
- ################################################################################
-
# Path_to_tests can also replaced by setting the RGT_PATH_TO_TESTS environment variable
Path_to_tests = /some/path/to/my/applications
@@ -122,7 +125,7 @@ Set a scratch area for this specific instance of the harness (a default is set f
export RGT_PATH_TO_TESTS=/some/path/to/my/applications
-The latest version of the harness supports command line tasks as well as input file tasks.
+The harness supports command line tasks as well as input file tasks.
If no tasks are provided in the input file, it will use the command line mode.
To launch via the command line, use a command like the following:
@@ -140,15 +143,13 @@ To launch tasks in the input file instead of the command-line, add lines like th
harness_task check_out_tests
harness_task start_tests
harness_task stop_tests
- harness_task display_status
-
When using the checkout mode, the application source repository will be cloned to the */* directory for all the tests,
but no tests will be run.
If the repository already exists, no action will be taken.
Updating the repo via ``git pull`` or ``git fetch`` should be done outside of the test harness.
-After using the start mode, results of the most recent test run can be found in *///Run_Archive/*.
+After using the start mode, results can be found in *///Run_Archive/*.
Results of the most recent test run can be found in the *///Run_Archive/latest* symbolic link.
.. note::
@@ -169,33 +170,37 @@ The primary OTH driver script, ``runtests.py``, supports the following command-l
.. code-block::
- -h,--help show help message and exit
- -i,--inputfile INPUTFILE Input file name (default: rgt.input)
- -c,--configfile CONFIGFILE Configuration file name (default: ${OLCF_HARNESS_MACHINE}.ini)
- -l,--loglevel LOGLEVEL Logging level (default: NOTSET)
- Options: [NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL]
- -o,--output {screen,logfile} Destination for harness stdout/stderr messages (default: 'screen')
- Options: [screen,logfile]
- 'screen' - print messages to console (default)
- 'logfile' - print messages to log file
- -m,--mode MODE [MODE ...] Specify the mode(s) to run the harness with (default: 'use_input_file')
- Options: [use_input_file,checkout,start,stop,status]
- 'use_input_file' - use tasks defined in the input file
- 'checkout' - checkout application tests listed in input file
- 'start' - start application tests listed in input file
- 'stop' - stop application tests listed in input file
- 'status' - check status of application tests listed in input file
-
- --fireworks Use FireWorks to run harness tasks (beta)
- -sb, --separate-build-stdio Separate output from build into build_out.stderr.txt and build_out.stdout.txt
- --shuffle Shuffle the order of tests before launching
- --reuse-first-build Reuse the first build in a chain of resubmitting tests for all subsequent submissions, per-test.
- --reuse-build-from-id Re-use the build from the specified test ID.
+ -i INPUTFILE, --inputfile INPUTFILE
+ Input file name (default: rgt.input)
+ --shuffle
+ Shuffle the order of tests before launching.
+ -c CONFIGFILE, --configfile CONFIGFILE
+ Configuration file name (default: master.ini)
+ -l {NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL}, --loglevel {NOTSET,DEBUG,INFO,WARNING,ERROR,CRITICAL}
+ Output logging level (default: WARNING)
+ -o {screen,logfile}, --output {screen,logfile}
+ Destination for harness stdout/stderr messages:
+ 'screen' - print messages to console (default)
+ 'logfile' - print messages to log file
+ -m {task} [{task} ...], --mode {task} [{task} ...]
+ Harness task:
+ 'checkout' - checkout application tests listed in input file
+ 'start' - start application tests listed in input file
+ 'stop' - stop application tests listed in input file
+ 'status' - check status of application tests listed in input file
+ --fireworks
+ Use FireWorks to run harness tasks
+ -sb, --separate-build-stdio
+ Separate output from build into build_out.stderr.txt and build_out.stdout.txt
+ --reuse-first-build
+ If running a test that can resubmit, re-use the first instance of the build for all tests in that chain.
+ --reuse-build-from-id REUSE_BUILD_FROM_ID
+ Re-use the build from the specified test ID.
+ --app-filter APP_FILTER
+ A comma-separated list of regular expressions or strings used to select specific applications from the provided input file.
+ --test-filter TEST_FILTER
+ A comma-separated list of regular expressions or strings used to select specific tests from the provided input file.
-.. note::
-
- The ``--loglevel`` flag currently does not apply to all output from the OTH.
- This issue is tracked by `Issue 130 `_.
.. _runtime_configurable_parameters:
@@ -208,9 +213,9 @@ For example, *git_reps_branch* is a parameter in *$OLCF_HARNESS_MACHINE.ini* tha
The *RGT_GIT_REPS_BRANCH* environment variable can be used to override this value at launch time.
The general precedence of configuration options from lowest to highest is:
-1. *$OLCF_HARNESS_MACHINE.ini*
+1. *$OLCF_HARNESS_MACHINE.ini* (lowest priority)
2. User-set environment variables (ie, *RGT_GIT_REPS_BRANCH*, *RGT_PROJECT_ID*)
-3. *///Scripts/rgt_test_input.ini*
+3. *///Scripts/rgt_test_input.ini* (highest priority)
The specific parameters are defined in :ref:`section_new_test` and :ref:`section_new_machine`.
@@ -239,6 +244,8 @@ There are 4 directories referenced in this section:
- **$RESULTS_DIR** - the directory used to launch the job and store relevant output, equal to **///Run_Archive/**
- **$STATUS_DIR** - the directory used to store harness status files, equal to **///Status/**
+**$RESULTS_DIR** should be considered the best starting point, as it contains symbolic links to the other 3 directories.
+
Build, Submit, and Check Output
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/_sources/user_guide/utilities.rst.txt b/docs/_sources/user_guide/utilities.rst.txt
index aa28f3c8..369efe8b 100644
--- a/docs/_sources/user_guide/utilities.rst.txt
+++ b/docs/_sources/user_guide/utilities.rst.txt
@@ -10,7 +10,7 @@ For example, many of these scripts handle keeping the remote database up-to-date
These scripts are documented below.
rgt_archive_utility.py
-========
+======================
The ``rgt_archive_utility.py`` script allows you to "archive" a test.
What "archive" actually means is it allows you to select when to keep or discard a test's build and work directories, and will copy the test into a single location on the file system, without needing sym-links between the Run_Archive and scratch areas.
@@ -94,7 +94,7 @@ The ``--help`` message for the ``rgt_archive_utility.py`` script is provided bel
update_databases.py
-========
+===================
The ``update_databases.py`` script retrieves all incomplete tests from the remote database (ie, an InfluxDB instance), and tries to determine if that test has completed, but did not log its completion message.
This script has support for the Slurm job scheduler, and will look for the job ID of the given test, to see if it completed.
@@ -154,7 +154,7 @@ This script was written with Cron usage in mind, so the following list of ``--lo
add_comment_to_databases.py
-========
+===========================
The ``add_comment_to_databases.py`` script adds a comment to a specific test instance in the remote database (ie, an InfluxDB instance).
The ``--help`` message for the ``add_comment_to_databases.py`` script is provided below.
@@ -182,7 +182,7 @@ This script requires the same environment variables as the core harness requires
Defaults to most recent event.
report_to_databases.py
-========
+======================
The ``report_to_databases.py`` script enables you to further utilize a remote database to store custom, non-harness metrics.
The ``--help`` message for the ``report_to_databases.py`` script is provided below.
diff --git a/docs/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/docs/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css
new file mode 100644
index 00000000..eb19f698
--- /dev/null
+++ b/docs/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css
@@ -0,0 +1 @@
+.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem}
diff --git a/docs/_sphinx_design_static/design-tabs.js b/docs/_sphinx_design_static/design-tabs.js
new file mode 100644
index 00000000..36b38cf0
--- /dev/null
+++ b/docs/_sphinx_design_static/design-tabs.js
@@ -0,0 +1,27 @@
+var sd_labels_by_text = {};
+
+function ready() {
+ const li = document.getElementsByClassName("sd-tab-label");
+ for (const label of li) {
+ syncId = label.getAttribute("data-sync-id");
+ if (syncId) {
+ label.onclick = onLabelClick;
+ if (!sd_labels_by_text[syncId]) {
+ sd_labels_by_text[syncId] = [];
+ }
+ sd_labels_by_text[syncId].push(label);
+ }
+ }
+}
+
+function onLabelClick() {
+ // Activate other inputs with the same sync id.
+ syncId = this.getAttribute("data-sync-id");
+ for (label of sd_labels_by_text[syncId]) {
+ if (label === this) continue;
+ label.previousElementSibling.checked = true;
+ }
+ window.localStorage.setItem("sphinx-design-last-tab", syncId);
+}
+
+document.addEventListener("DOMContentLoaded", ready, false);
diff --git a/docs/_sphinx_design_static/sphinx-design.min.css b/docs/_sphinx_design_static/sphinx-design.min.css
new file mode 100644
index 00000000..860c36da
--- /dev/null
+++ b/docs/_sphinx_design_static/sphinx-design.min.css
@@ -0,0 +1 @@
+.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em .6em .5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700}
diff --git a/docs/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/docs/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css
new file mode 100644
index 00000000..eb19f698
--- /dev/null
+++ b/docs/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css
@@ -0,0 +1 @@
+.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem}
diff --git a/docs/_static/design-tabs.js b/docs/_static/design-tabs.js
new file mode 100644
index 00000000..36b38cf0
--- /dev/null
+++ b/docs/_static/design-tabs.js
@@ -0,0 +1,27 @@
+var sd_labels_by_text = {};
+
+function ready() {
+ const li = document.getElementsByClassName("sd-tab-label");
+ for (const label of li) {
+ syncId = label.getAttribute("data-sync-id");
+ if (syncId) {
+ label.onclick = onLabelClick;
+ if (!sd_labels_by_text[syncId]) {
+ sd_labels_by_text[syncId] = [];
+ }
+ sd_labels_by_text[syncId].push(label);
+ }
+ }
+}
+
+function onLabelClick() {
+ // Activate other inputs with the same sync id.
+ syncId = this.getAttribute("data-sync-id");
+ for (label of sd_labels_by_text[syncId]) {
+ if (label === this) continue;
+ label.previousElementSibling.checked = true;
+ }
+ window.localStorage.setItem("sphinx-design-last-tab", syncId);
+}
+
+document.addEventListener("DOMContentLoaded", ready, false);
diff --git a/docs/_static/js/versions.js b/docs/_static/js/versions.js
new file mode 100644
index 00000000..4958195e
--- /dev/null
+++ b/docs/_static/js/versions.js
@@ -0,0 +1,228 @@
+const themeFlyoutDisplay = "hidden";
+const themeVersionSelector = true;
+const themeLanguageSelector = true;
+
+if (themeFlyoutDisplay === "attached") {
+ function renderLanguages(config) {
+ if (!config.projects.translations.length) {
+ return "";
+ }
+
+ // Insert the current language to the options on the selector
+ let languages = config.projects.translations.concat(config.projects.current);
+ languages = languages.sort((a, b) => a.language.name.localeCompare(b.language.name));
+
+ const languagesHTML = `
+
Each test’s Scripts directory should contain a test input file named rgt_test_input.ini.
+
Each test’s Scripts directory should contain a test input file named rgt_test_input.ini or rgt_test_input.yaml.
The test input file contains information that is used by the OTH to build, submit, and check the results of application tests.
-The test input file follows the Python3 configparser file format.
-The fields in the [DEFAULT] section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
+The ini file format follows the Python3 configparser file format, while yaml file format is standard YAML.
+
+
+
+
The fields in the [DEFAULT] section can be used in the other sections of the configuration file and are useful for defining a variable that is re-used in multiple sections.
All the fields in the [Replacements] section can be used in the job script template and will be replaced when creating the batch script (see Job Script Template section below).
Variables in [Replacements] cannot be referenced from [EnvVars].
The fields in the [EnvVars] section allow you to set environment variables that all stages of your test will be able to use.
@@ -329,28 +335,30 @@
The yaml file format was added to the OTH in 2026, and allows for more powerful templating with Jinja2 template support.
+There are slight changes to the yaml section and variable names relative to the ini file format.
+The fields in the variables section can be used in the replacements section of the configuration file by writing a Python format string, as seen below.
+All the fields in the replacements section can be used in the job script template and will be replaced when creating the batch script (see Job Script Template section below).
+Unlike ini file format, the yaml file does not support [EnvVars], as using this feature is not good practice.
+See Best Practices section for other test input file recommendations.
+The following is a sample input for the single node test of the hello_mpi application mentioned above:
+
variables:
+# The variables section defines variables that can be re-used in Replacements or EnvVars
+# These variables are not automatically used as replacements
+my_job_name:hello_mpi_c
+
+replacements:
+#### The following variables are called "built-in", variables the harness knows to look for
+# These are required for every test:
+nodes:1
+# Must use quotes (either single or double) when providing Python format strings
+# Otherwise, YAML thinks you're defining a dictionary
+job_name:'{my_job_name}'
+batch_filename:'run_{my_job_name}.sh'
+build_cmd:./build_{my_job_name}.sh
+check_cmd:./check_{my_job_name}.sh
+
+#### Optional built-in replacements:
+# Set to 1 if you want to allow this test to be resubmitted automatically with ``runtests.py --mode start ...``
+resubmit:0
+# Used in conjunction with resubmit argument to limit total submissions/runs of a test (inclusive of initial run)
+# Set to 0 (or don't define) for indefinite resubmissions
+max_submissions:3
+
+# Some build processes will auto-generate a job script, so you can optionally disable the OTH's batch script generation:
+#use_batch_template: 0
+
+# project_id and batch_queue should only be used if a specific partition or account is always required
+#project_id: abc123
+#batch_queue: my_special_partition
+
+#### Variables that used to be required and may be useful, but are no longer required:
+report_cmd:'./report_{my_job_name}.sh'
+walltime:10
+executable_path:hello
+
+#### The following are user-defined and used for Key-Value replacements in the job template
+total_processes:16
+processes_per_node:16
+
+
+
As with ini, yaml file format supports the <obtain_from_environment> option.
+Unlike ini, yaml file format does not support using the value of one replacement to define another, it only supports using variables in the definition of a replacement.
The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script, and (4) a reporting script.
+
The OTH requires each application test to provide (1) a build script, (2) a job script template, (3) a check script.
These scripts should be placed in the locations described in Repository structure.
-The build, check, and reporting scripts may also be set to Linux commands such as /usr/bin/echo.
-This is useful in cases where a script is not needed.
-For example, a test that relies on standard system-provided tools can set the build script to /usr/bin/echo to remove the need to have an empty build script.
-If the OTH cannot find the scripts specified by the test input file (rgt_test_input.ini), it will fail to launch.
+A test that relies on standard system-provided binaries can set the build script to /usr/bin/echo to remove the need to have an empty build script.
+If the OTH cannot find the scripts specified by the test input file (rgt_test_input.[yaml,ini]), it will fail to launch.
The OTH will generate the batch job script from the job script template by replacing keywords
-of the form __keyword__ with the values specified in the test input [Replacements] section.
+of the form __keyword__ with the values specified in the test input file’s replacements section.
Additionally, the OTH automatically provides several replacement keywords for the job script to use, described below:
results_dir: absolute path to the test’s Run_Archive directory, which is where the job is launched from, and where it typically copies results to
Generally, these should be used to set environment variables, as shown in the template below.
-
The job script template must be named appropriately to match the specific scheduler of the target machine.
-For SLURM systems, use slurm.template.x as the name.
-For LSF systems, use lsf.template.x.
-An example SLURM template script for the hello_mpi application follows:
+
The job script template must be named appropriately to match the specific scheduler of the target machine AND rgt_test_input.[yaml,ini] file format, as detailed below.
+
+
+
+
+
+
+
+
Scheduler
+
INI test input
+
YAML test input
+
+
+
+
Slurm
+
slurm.template.x
+
slurm.template.yaml
+
+
LSF
+
lsf.template.x
+
lsf.template.yaml
+
+
PBS
+
pbs.template.x
+
pbs.template.yaml
+
+
+
+
An example Slurm template script for the hello_mpi application for both INI+x and YAML+Jinja2 format is provided below.
+In simple cases, there is little functional difference between the two templating formats, but YAML+Jinja2 has far greater power with handling complex test inputs like arrays.
#!/bin/bash -l
+#SBATCH -J {{job_name}}
+#SBATCH -N {{nodes}}
+#SBATCH -t {{walltime}}
+#SBATCH -o {{job_name}}.o%j
+
+# Define environment variables needed
+exportSCRIPTS_DIR="{{scripts_dir}}"
+exportWORK_DIR="{{working_dir}}"
+exportRESULTS_DIR="{{results_dir}}"
+exportHARNESS_ID="{{harness_id}}"
+exportBUILD_DIR="{{build_dir}}"
+
+exportEXECUTABLE="{{executable_path}}"
+
+echo"Printing test directory environment variables:"
+env|fgrepRGT_APP_SOURCE_
+env|fgrepRGT_TEST_
+echo
+
+# Placing the environment setup script in a shared location reduces code duplication
+# and ensures you have the same environment in building & running
+source$BUILD_DIR/Common_Scripts/setup_env.sh
+
+# Ensure we are in the starting directory
+cd$SCRIPTS_DIR
+
+# Make the working scratch space directory.
+[-d$WORK_DIR]&&mkdir-p$WORK_DIR
+
+# Change directory to the working directory.
+cd$WORK_DIR
+
+env&>job.environ
+scontrolshowhostnames&>job.nodes
+ldd$BUILD_DIR/bin/$EXECUTABLE&>ldd.log
+
+# Run the executable.
+log_binary_execution_time.py--scriptsdir$SCRIPTS_DIR--uniqueid$HARNESS_ID--modestart
+
+set-x
+srun-n{{total_processes}}-N{{nodes}}$BUILD_DIR/bin/$EXECUTABLE
+# If wanted, save the exit code & use it to exit the job with
+exit_code=$?
+set+x
+
+log_binary_execution_time.py--scriptsdir$SCRIPTS_DIR--uniqueid$HARNESS_ID--modefinal
+
+# Ensure we return to the starting directory.
+cd$SCRIPTS_DIR
+
+# Copy the output and results back to the $RESULTS_DIR
+# Depending on the size of files in $WORK_DIR, you may want to change this
+cp-rf$WORK_DIR/*$RESULTS_DIR
+cp$BUILD_DIR/output_build*.txt$RESULTS_DIR
+
+# Check the final results.
+check_executable_driver.py-p$RESULTS_DIR-i$HARNESS_ID
+
+# Resubmit if needed:
+# If you always want tests to resubmit if ``.kill_test`` is not present,
+# then remove the conditional around calling ``test_harness_driver.py``.
+case{{resubmit}}in
+0)
+echo"No resubmit";;1)
-test_harness_driver.py-r__max_submissions__;;
+test_harness_driver.py-r{{max_submissions}};;esac
+
+exit$exit_code
-
Using the job template above, the job will be submitted from the test Run_Archive/ directory and starts there.
+
+
+
Using the job template above, the job will be submitted from the test Run_Archive/ directory and starts from there.
This is $RESULTS_DIR in the job template.
-The executable should then be run from $WORK_DIR directory, which is a scratch workspace derived from $RGT_PATH_TO_SSPACE.
+The executable should then be invoked from $WORK_DIR directory, which is a scratch workspace derived from $RGT_PATH_TO_SSPACE.
One can access or copy any files relative to the Scripts/ directory using the $SCRIPT_DIR environment variable.
For example, if one stores a CorrectResults directory at the same level as Scripts and Run_Archive for a test case,
-it can be be copied by adding the line
+it can be be copied by adding the following line in the job script:
cp-a${SCRIPT_DIR}/../CorrectResults${WORK_DIR}/
-
inside the job script.
The environment variable $EXECUTABLE is also populated based on executable_path entry in rgt_test_input.ini file.
The executable may still be inside $BUILD_DIR from the previous step,
so one would need to either copy it to $WORK_DIR or provide the absolute path in the job script such as $BUILD_DIR/$EXECUTABLE.
The check script can be a shell script, Python script, or other executable command.
-This must be an absolute path to a command (ie, /usr/bin/echo instead of echo).
+The check command is prefixed with $SCRIPTS_DIR, so check_cmd=check.sh is called as $SCRIPTS_DIR/check.sh.
Check scripts are used to verify that application tests ran as expected, and thus use standardized return codes to inform the OTH on the test result.
Checking performance is optional but recommended for most tests.
The check script return value should be one of the following:
Like the check script, the report script can be a shell script, Python script, or other executable command.
-Report scripts are generally used to compute performance metrics from the run.
+
Optionally, a reporting script may be provided.
+Like the check script, the report script can be a shell script, Python script, or other executable command.
+Report scripts are generally used to solely gather performance metrics from the run.
The exit code of report scripts is not checked by the OTH.
The report script is launched from $RESULTS_DIR and stdout/stderr is captured in $RESULTS_DIR/output_report.txt.
-
-
Note
-
In many cases, the check script serves the function of both the check and report script.
-In that event, report scripts often just exit0.
-An alternative to a no-op bash script, you may use /usr/bin/echo on most Linux systems.
This check script is generic and should be able to be re-used in multiple tests, so let’s put it in Source/Common_Scripts/check_hello_world.sh.
-
The OTH also wants a report script, but there’s not much to report here.
-You can either create a script that immediately exits, or just link to your check script.
-Here, we will just link to the check script.
-
The Slurm template and check and report scripts are required in the Scripts directory, so we use symbolic links to achieve this:
+
The Slurm template and check scripts are required in the Scripts directory, so we use symbolic links to achieve this:
# from mpi-testscdhello_world_n0001/Scripts
-ln-s../../Source/Common_Scripts/slurm.template.x.
+ln-s../../Source/Common_Scripts/slurm.template.j2.
ln-s../../Source/Common_Scripts/check_hello_world.sh./check.sh
-ln-s../../Source/Common_Scripts/check_hello_world.sh./report.sh
To expand to a 2-node Hello, World! test, we can just copy the Scripts directory from the single-node test, then modify the rgt_test_input.ini to specify 2 nodes instead of 1.
@@ -761,10 +929,10 @@
During a test run, the environment is independently set up during the build and run stages.
+
During a test run, the environment is independently set up during the build and run stages (e.g., the job submission is not done from within the build environment).
If the build script and job script each contain several moduleload statements, there is a chance that those can diverge.
To centralize where the environment is set to a single file, place a script containing the module commands and environment modifications in the build directory,
-and source that script from the build and job scripts.
+and source that script from both the build and job scripts.
For the build script, this can be accomplished as simply sourceenv.sh, if the script is in the top level of the Source directory.
For the job script, this can be accomplished by source$BUILD_DIR/env.sh, if the $BUILD_DIR environment variable is defined as in the Job Script Template section above.
The OTH leaves behind status files containing event metadata like timestamp, filesystem paths for work, scratch and archive directories, test name, error code, etc.
This information can additionally be logged to a supported database.
The OTH currently provides support for InfluxDB and Kafka (with Druid database backend).
diff --git a/docs/user_guide/launching.html b/docs/user_guide/launching.html
index ce952c35..5837e599 100644
--- a/docs/user_guide/launching.html
+++ b/docs/user_guide/launching.html
@@ -15,6 +15,7 @@
+
@@ -38,6 +39,7 @@
+
@@ -267,8 +269,11 @@
Create a directory where you will place input files. No computation will be done here:
+
Create a directory where you will place input files, which will instruct the OTH on which tests to launch:
mkdirsummit_testshot
cdsummit_testshot
-
Prepare an input file of tests (e.g., rgt.input.summit).
-In the file, set Path_to_tests to the location where you would like application source and run files to be kept
-(note that the directory provided must be an existing directory on a file system visible to the current machine).
-Alternatively, set the RGT_PATH_TO_TESTS environment variable.
-Next, provide one or more tests to run in the format Test=<app-name><test-name>.
-In this example for Summit, the application hello_mpi is used and we specify two tests: c_n001 and c_n002.
+
Prepare an input file of tests in this directory (e.g., rgt.input.summit).
+Provide one or more tests to run in the format Test=<app-name><test-name>.
+In this example for Summit, the application hello_mpi is used and we specify two tests: c_n001 and c_n002.
+Optionally, set Path_to_tests in this file to the location where you would like application source and run files to be kept
+(note that the directory provided must be an existing directory).
+Alternatively, you may set the RGT_PATH_TO_TESTS environment variable.
+Either the RGT_PATH_TO_TESTS environment variable or Path_to_tests in the input file must be provided.
Note
Tests may be hosted in GitHub/GitLab repositories, or may be placed on the file system in the directory specified by Path_to_tests/RGT_PATH_TO_TESTS.
@@ -321,11 +330,7 @@
################################################################################
-# Set the path to the top level of the application directory. #
-################################################################################
-
-# Path_to_tests can also replaced by setting the RGT_PATH_TO_TESTS environment variable
+
# Path_to_tests can also replaced by setting the RGT_PATH_TO_TESTS environment variablePath_to_tests=/some/path/to/my/applications
Test=hello_mpic_n001
@@ -347,7 +352,7 @@
The latest version of the harness supports command line tasks as well as input file tasks.
+
The harness supports command line tasks as well as input file tasks.
If no tasks are provided in the input file, it will use the command line mode.
To launch via the command line, use a command like the following:
# Preferred to checkout separately, to verify that the checkout was successful
@@ -360,14 +365,13 @@
Each OTH test run consists of 4 primary stages – build, submit, run, and check, as can be seen in Overview of the Test Harness.
diff --git a/docs/user_guide/overview.html b/docs/user_guide/overview.html
index 7187ffd2..7c62f659 100644
--- a/docs/user_guide/overview.html
+++ b/docs/user_guide/overview.html
@@ -15,6 +15,7 @@
+
@@ -38,6 +39,7 @@
+
diff --git a/docs/user_guide/utilities.html b/docs/user_guide/utilities.html
index c67fa0f8..21c47290 100644
--- a/docs/user_guide/utilities.html
+++ b/docs/user_guide/utilities.html
@@ -15,6 +15,7 @@
+
@@ -38,6 +39,7 @@
+
diff --git a/harness/__init__.py b/harness/__init__.py
index f659b7f8..e8b628d7 100644
--- a/harness/__init__.py
+++ b/harness/__init__.py
@@ -2,8 +2,8 @@
"bin",
"libraries",
"utilities",
- "fundamental_types",
- "machine_types"
+ "machine_types",
+ "schedulers"
]
version = 3.1
diff --git a/harness/bin/check_executable_driver.py b/harness/bin/check_executable_driver.py
index b31280b1..b6cbbd77 100755
--- a/harness/bin/check_executable_driver.py
+++ b/harness/bin/check_executable_driver.py
@@ -6,6 +6,10 @@
import subprocess
import getopt
import string
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
# Harness imports
from libraries.apptest import subtest
diff --git a/harness/bin/log_binary_execution_time.py b/harness/bin/log_binary_execution_time.py
index feff0102..3acf1481 100755
--- a/harness/bin/log_binary_execution_time.py
+++ b/harness/bin/log_binary_execution_time.py
@@ -3,6 +3,10 @@
import argparse
import sys
import os
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
from libraries.subtest_factory import SubtestFactory
from libraries.status_file_factory import StatusFileFactory
diff --git a/harness/bin/runtests.py b/harness/bin/runtests.py
index 12bb51f0..42c1c7d3 100755
--- a/harness/bin/runtests.py
+++ b/harness/bin/runtests.py
@@ -5,6 +5,10 @@
import argparse
import os
import sys
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
# My harness package imports
from libraries import input_files
diff --git a/harness/bin/test_harness_driver.py b/harness/bin/test_harness_driver.py
index 395dda1d..d9751e05 100755
--- a/harness/bin/test_harness_driver.py
+++ b/harness/bin/test_harness_driver.py
@@ -23,6 +23,10 @@
import sys
from shlex import split
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
# Harness imports
from libraries.subtest_factory import SubtestFactory
@@ -35,7 +39,6 @@
from libraries import status_file
from libraries.rgt_loggers import rgt_logger_factory
from machine_types.machine_factory import MachineFactory
-from machine_types.base_machine import SetBuildRTEError
MODULE_THRESHOLD_LOG_LEVEL = "DEBUG"
"""str : The logging level for this module. """
@@ -118,14 +121,14 @@ def backup_status_file(test_status_dir):
#
# Now copy the status file to the backup file.
#
- if os.path.exists(src):
+ if Path(src).exists():
shutil.copyfile(src, dest)
def read_job_file(test_status_dir):
""" Read test_status_dir/job_id.txt to get job id """
job_id = "0"
fpath = os.path.join(test_status_dir, layout.job_id_filename)
- if os.path.exists(fpath):
+ if Path(fpath).exists():
jfile = open(fpath, "r")
job_line = jfile.readline()
jfile.close()
@@ -160,15 +163,22 @@ def auto_generated_scripts(harness_config,
build_exit_value = 0
if actions['build']:
# Build the executable for this test on the specified machine
+ scripts_dir = os.getcwd()
jstatus.log_event(status_file.StatusFile.EVENT_BUILD_START)
try:
build_exit_value = mymachine.build_executable()
- except SetBuildRTEError as error:
- message = f"Unable to set the build runtime environnment."
- message += error.message
- a_logger.doCriticalLogging(message)
- finally:
- jstatus.log_event(status_file.StatusFile.EVENT_BUILD_END, build_exit_value)
+ except KeyboardInterrupt:
+ a_logger.doCriticalLogging(f"Detected CTRL+C, aborting build.")
+ os.chdir(scripts_dir)
+ build_exit_value = 21
+ pass
+ except Exception as e:
+ a_logger.doCriticalLogging(f"Exception generated during build, aborting test launch: {e}.")
+ os.chdir(scripts_dir)
+ build_exit_value = 1
+ pass
+
+ jstatus.log_event(status_file.StatusFile.EVENT_BUILD_END, build_exit_value)
#-----------------------------------------------------
# In this section we run the the binary. -
# -
@@ -411,7 +421,7 @@ def test_harness_driver(argv=None):
#
if do_submit:
kill_file = apptest.get_path_to_kill_file()
- if os.path.exists(kill_file):
+ if Path(kill_file).exists():
import shutil
message = f'The kill file {kill_file} exists. It must be removed to run this test.\n'
message += "Stopping test cycle."
@@ -433,7 +443,7 @@ def test_harness_driver(argv=None):
# Requires implementation in base_machine.py and layout_of_apps_directory.py
# Environment variable set by user or by runtests.py
build_dir = apptest.get_path_to_workspace_build()
- if 'RGT_REUSE_BUILD_FROM' in os.environ and os.path.exists(os.environ['RGT_REUSE_BUILD_FROM']):
+ if 'RGT_REUSE_BUILD_FROM' in os.environ and Path(os.environ['RGT_REUSE_BUILD_FROM']).exists():
build_dir = os.environ['RGT_REUSE_BUILD_FROM']
elif 'RGT_REUSE_BUILD_FROM' in os.environ:
# otherwise, update this envvar to the current build directory
diff --git a/harness/fundamental_types/__init__.py b/harness/fundamental_types/__init__.py
deleted file mode 100644
index 4fd9c998..00000000
--- a/harness/fundamental_types/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-__all__ = [
- "rgt_state"
- ]
-
-version = 2.0
diff --git a/harness/libraries/__init__.py b/harness/libraries/__init__.py
index 67ffa905..629fc238 100644
--- a/harness/libraries/__init__.py
+++ b/harness/libraries/__init__.py
@@ -9,7 +9,8 @@
'regression_test',
'status_file',
'repositories',
- 'rgt_loggers'
+ 'rgt_loggers',
+ 'rgt_state',
'command_line',
'get_machine_name',
'status_file_factory',
diff --git a/harness/libraries/apptest.py b/harness/libraries/apptest.py
index 86e76bd9..99e89874 100644
--- a/harness/libraries/apptest.py
+++ b/harness/libraries/apptest.py
@@ -14,6 +14,7 @@
import copy
import re
from types import *
+from pathlib import Path
# NCCS Test Harness Package Imports
from libraries.harness_internal_config import harness_modes
@@ -507,12 +508,12 @@ def _start_test(self,
if reuse_build_from_id:
# Check if test_id exists in Run_Archive
target_build_runarchive_path = os.path.join(self.get_path_to_test(), self.test_run_archive_dirname, reuse_build_from_id)
- if not os.path.exists(target_build_runarchive_path):
+ if not Path(target_build_runarchive_path).exists():
self.logger.doCriticalLogging(f"Could not find test_id {reuse_build_from_id} in {target_build_runarchive_path}.")
return 1
# Check if build_directory from test_id still exists
target_builddir_path = os.path.realpath(os.path.join(target_build_runarchive_path, self.test_build_dirname))
- if not os.path.exists(target_builddir_path):
+ if not Path(target_builddir_path).exists():
self.logger.doCriticalLogging(f"Could not find build_directory from test_id {reuse_build_from_id} in {target_builddir_path}.")
return 1
# if all checks pass, we're good to set it
@@ -533,9 +534,6 @@ def _start_test(self,
message = ( "The command '{cmd}' has exited with a failure.\n"
"The exit return value is {value}.\n").format(cmd=starttestcomand,value=exit_status)
self.logger.doCriticalLogging(message)
-
-
- string1 = "Command failed: " + starttestcomand
return 1
else:
message = "'{cmd}' has executed sucessfully.\n".format(cmd=starttestcomand)
@@ -571,7 +569,7 @@ def _run_db_extensions(self):
# this chunk of code to grab a job id taken from status_file.py
job_id = StatusFile.NO_VALUE
file_job_id = self.get_path_to_job_id_file()
- if os.path.exists(file_job_id):
+ if Path(file_job_id).exists():
file_ = open(file_job_id, 'r')
job_id_ = file_.read()
file_.close()
@@ -639,7 +637,7 @@ def _get_run_timestamp(self, event=StatusFile.EVENT_CHECK_END):
check_status_file = f"{self.get_path_to_test()}/{self.test_status_dirname}/{self.get_harness_id()}/"
check_status_file += f"{StatusFile.EVENT_DICT[event][0]}"
- if not os.path.exists(f"{check_status_file}"):
+ if not Path(f"{check_status_file}").exists():
self.logger.doWarningLogging(f"Couldn't find required file for post-run time logging: {check_status_file}")
return -1
with open(f"{check_status_file}", 'r') as check_fstr:
@@ -657,7 +655,7 @@ def _get_event_time(self, event=StatusFile.EVENT_CHECK_END):
status_file = f"{self.get_path_to_test()}/{self.test_status_dirname}/{self.get_harness_id()}/"
status_file += f"{StatusFile.EVENT_DICT[event][0]}"
- if not os.path.exists(f"{status_file}"):
+ if not Path(f"{status_file}").exists():
self.logger.doWarningLogging(f"Couldn't find required file event time fetching: {status_file}")
return -1
with open(f"{status_file}", 'r') as fstr:
@@ -671,7 +669,7 @@ def _get_time_diff_of_status_files(self, start_event_file, end_event_file):
for targ in [ f"{status_dir}/{start_event_file}", \
f"{status_dir}/{end_event_file}" ]:
- if not os.path.exists(f"{targ}"):
+ if not Path(f"{targ}").exists():
self.logger.doWarningLogging(f"Couldn't find required file for time logging: {targ}")
return -1
start_timestamp = ''
@@ -697,7 +695,7 @@ def _get_metrics(self):
metrics = {}
app_name = self.getNameOfApplication()
test_name = self.getNameOfSubtest()
- if not os.path.isfile('metrics.txt'):
+ if not Path('metrics.txt').is_file():
self.logger.doWarningLogging(f"File metrics.txt not found")
return metrics
with open('metrics.txt', 'r') as metric_f:
@@ -731,7 +729,7 @@ def _get_node_health(self):
app_name = self.getNameOfApplication()
test_name = self.getNameOfSubtest()
- if not os.path.isfile('nodecheck.txt'):
+ if not Path('nodecheck.txt').is_file():
self.logger.doInfoLogging(f"File nodecheck.txt not found.")
return node_healths
self.logger.doDebugLogging("Processing file nodecheck.txt.")
diff --git a/harness/libraries/config_file.py b/harness/libraries/config_file.py
index a80def93..1807a4eb 100644
--- a/harness/libraries/config_file.py
+++ b/harness/libraries/config_file.py
@@ -2,6 +2,7 @@
import os
import configparser
import logging
+from pathlib import Path
from libraries.rgt_utilities import set_harness_environment
from libraries.rgt_loggers import rgt_logger_factory
@@ -24,6 +25,7 @@ def __init__(self,
self.__site_vars = {}
self.__testshot_vars = {}
self.__logger = logger
+ self.__harness_dir = Path(__file__).resolve().parent.parent.parent
if not logger:
self.__logger = rgt_logger_factory.create_rgt_logger(
@@ -43,21 +45,19 @@ def __init__(self,
base_filename = os.path.basename(self.__configFileName)
if base_filename == self.__configFileName:
- # Only base file given, resolve full path by searching CWD, then OLCF_HARNESS_DIR/configs
- working_dir_config = os.path.join(os.getcwd(), self.__configFileName)
- if os.path.isfile(working_dir_config):
- self.__configFileName = os.path.abspath(working_dir_config)
- elif 'OLCF_HARNESS_DIR' in os.environ:
- harness_dir = os.environ['OLCF_HARNESS_DIR']
- harness_dir_config = os.path.join(harness_dir, "configs", self.__configFileName)
- if os.path.isfile(harness_dir_config):
- self.__configFileName = harness_dir_config
+ # Only base file given, resolve full path by searching CWD, then harness_root/configs
+ working_dir_config = Path(os.getcwd(), self.__configFileName)
+ harness_dir_config = Path(self.__harness_dir, "configs", self.__configFileName)
+ if working_dir_config.is_file():
+ self.__configFileName = str(working_dir_config.resolve())
+ elif harness_dir_config.is_file():
+ self.__configFileName = str(harness_dir_config)
# Read the master config file
self.__read_config_file()
def __read_config_file(self):
- if os.path.isfile(self.__configFileName):
+ if Path(self.__configFileName).is_file():
self.__logger.doInfoLogging(f'reading harness config {self.__configFileName}')
master_cfg = configparser.ConfigParser()
master_cfg.read(self.__configFileName)
diff --git a/harness/libraries/input_files.py b/harness/libraries/input_files.py
index d7c2adb3..333a6919 100644
--- a/harness/libraries/input_files.py
+++ b/harness/libraries/input_files.py
@@ -4,6 +4,7 @@
import string
import os
import configparser
+from pathlib import Path
import re
# My harness package imports
@@ -162,7 +163,7 @@ def __read_file(self, input_file, app_filter=None, test_filter=None):
self.__logger.doDebugLogging(f"Validating if {test_path} (set via Path_to_tests) exists.")
if self.__path_to_tests:
self.__logger.doWarningLogging(f"Path_to_tests already set, ignoring Path_to_tests = {test_path}.")
- elif os.path.exists(test_path):
+ elif Path(test_path).exists():
self.__path_to_tests = test_path
else:
self.__logger.doCriticalLogging("Invalid path_to_test")
diff --git a/harness/libraries/layout_of_apps_directory.py b/harness/libraries/layout_of_apps_directory.py
index ec714bd4..eb40d5e4 100644
--- a/harness/libraries/layout_of_apps_directory.py
+++ b/harness/libraries/layout_of_apps_directory.py
@@ -27,6 +27,7 @@ class apptest_layout:
app_info_filename = 'application_info.txt'
test_info_filename = 'test_info.txt'
test_input_ini_filename = 'rgt_test_input.ini'
+ test_input_yaml_filename = 'rgt_test_input.yaml'
test_kill_filename = '.kill_test'
test_rc_filename = '.testrc'
test_status_filename = 'rgt_status.txt'
@@ -73,6 +74,7 @@ class apptest_layout:
'runarchive_dir' : os.path.join("${pdir}", "${app}", "${test}", test_run_archive_dirname, "${id}"),
'scripts_dir' : os.path.join("${pdir}", "${app}", "${test}", test_scripts_dirname),
'test_input_ini' : os.path.join("${pdir}", "${app}", "${test}", test_scripts_dirname, test_input_ini_filename),
+ 'test_input_yaml' : os.path.join("${pdir}", "${app}", "${test}", test_scripts_dirname, test_input_yaml_filename),
'kill_file' : os.path.join("${pdir}", "${app}", "${test}", test_scripts_dirname, test_kill_filename),
'status_dir' : os.path.join("${pdir}", "${app}", "${test}", test_status_dirname, "${id}"),
'job_id_file' : os.path.join("${pdir}", "${app}", "${test}", test_status_dirname, "${id}", job_id_filename),
@@ -110,23 +112,24 @@ def __init__(self,
def check_paths(self):
""" Returns False if the Source dir, Scripts dir, or test input ini files don't exist """
# Check that the Application dir exists
- if not os.path.exists(self.__apptest_layout['app']):
+ if not Path(self.__apptest_layout['app']).exists():
self.__logger.doErrorLogging(f"Could not find the Application root directory for App={self.__appname}, Test={self.__testname}.")
return False
# Check that the Application's Source dir exists
- if not os.path.exists(self.get_path_to_source()):
+ if not Path(self.get_path_to_source()).exists():
self.__logger.doErrorLogging(f"Could not find the Source directory for App={self.__appname}, Test={self.__testname}.")
return False
# Check that the Test dir exists
- if not os.path.exists(self.__apptest_layout['test']):
+ if not Path(self.__apptest_layout['test']).exists():
self.__logger.doErrorLogging(f"Could not find the test directory for App={self.__appname}, Test={self.__testname}.")
return False
# Check that the Scripts directory exists
- if not os.path.exists(self.get_path_to_scripts()):
+ if not Path(self.get_path_to_scripts()).exists():
self.__logger.doErrorLogging(f"Could not find the Scripts directory for App={self.__appname}, Test={self.__testname}.")
return False
# Check that the an rgt_test_ini.ini file exists
- if not (os.path.exists(self.__apptest_layout['test_input_ini'])):
+ if not (Path(self.__apptest_layout['test_input_ini']).exists() or \
+ Path(self.__apptest_layout['test_input_yaml']).exists()):
self.__logger.doErrorLogging(f"Could not find the test input file for App={self.__appname}, Test={self.__testname}.")
return False
return True
@@ -136,10 +139,14 @@ def get_harness_id(self):
return self.__testid
@property
- def path_of_test_input_file(self):
- """Returns the path to the subtest INI input file. """
+ def path_of_test_input_file_ini(self):
+ """Returns the path to the subtest input file. """
return self.__apptest_layout['test_input_ini']
+ @property
+ def path_of_test_input_file_yaml(self):
+ """Returns the path to the subtest input file. """
+ return self.__apptest_layout['test_input_yaml']
@property
def path_to_logfile(self) :
@@ -219,7 +226,7 @@ def get_path_to_workspace_build(self):
return None
# Redirect path to build if reusing from a existing test directory
if 'RGT_REUSE_BUILD_FROM' in os.environ and \
- os.path.exists(os.environ['RGT_REUSE_BUILD_FROM']):
+ Path(os.environ['RGT_REUSE_BUILD_FROM']).exists():
return os.environ['RGT_REUSE_BUILD_FROM']
else:
return os.path.join(self.__workspace, apptest_layout.test_build_dirname)
@@ -241,7 +248,7 @@ def create_test_status(self):
Create directory if it does not exist.
"""
spath = self.get_path_to_status()
- if not os.path.exists(spath):
+ if not Path(spath).exists():
os.makedirs(spath)
#
@@ -270,7 +277,7 @@ def create_test_runarchive(self):
# This path should be unique.
#
rpath = self.get_path_to_runarchive()
- if not os.path.exists(rpath):
+ if not Path(rpath).exists():
os.makedirs(rpath)
#
@@ -305,8 +312,8 @@ def create_workspace_links(self):
# if reusing a build, and the build directory doesn't exist
# if it does exist, it's probably set to the current test, and we can ignore it
if 'RGT_REUSE_BUILD_FROM' in os.environ and \
- os.path.exists(os.environ['RGT_REUSE_BUILD_FROM']) and \
- not os.path.exists(os.path.join(ws_dir, apptest_layout.test_build_dirname)):
+ Path(os.environ['RGT_REUSE_BUILD_FROM']).exists() and \
+ not Path(ws_dir, apptest_layout.test_build_dirname).exists():
# If re-using a build, also create a sym-link to the source build in the workspace
try_symlink(os.environ['RGT_REUSE_BUILD_FROM'], os.path.join(ws_dir, apptest_layout.test_build_dirname))
try_symlink(build_dir, os.path.join(ra_dir, apptest_layout.test_build_dirname))
@@ -365,7 +372,7 @@ def get_path_to_start_binary_time(self,uniqueid):
tmppath = os.path.join(self.__apptest_layout['status_dir'],
"start_binary_execution_timestamp.txt")
- if os.path.exists(tmppath):
+ if Path(tmppath).exists():
path = tmppath
return path
@@ -379,7 +386,7 @@ def get_path_to_end_binary_time(self,uniqueid):
tmppath = os.path.join(self.__apptest_layout['status_dir'],
"final_binary_execution_timestamp.txt")
- if os.path.exists(tmppath):
+ if Path(tmppath).exists():
path = tmppath
return path
diff --git a/harness/libraries/regression_test.py b/harness/libraries/regression_test.py
index 79e27af3..4e251151 100644
--- a/harness/libraries/regression_test.py
+++ b/harness/libraries/regression_test.py
@@ -13,7 +13,7 @@
# Harness package imports.
from libraries import apptest
from libraries.subtest_factory import SubtestFactory
-from fundamental_types.rgt_state import RgtState
+from libraries.rgt_state import RgtState
from libraries.rgt_loggers import rgt_logger_factory
from machine_types.machine_factory import MachineFactory
@@ -262,7 +262,9 @@ def __run_subtests_asynchronously(self):
# Submit futures by means of thread pool.
with concurrent.futures.ThreadPoolExecutor(max_workers=self.__num_workers) as executor:
for subtest in self.__app_subtests:
- future = executor.submit(apptest.do_application_tasks,
+ # gracefully handle keyboard interrupts in main thread
+ try:
+ future = executor.submit(apptest.do_application_tasks,
self.__launch_id,
subtest,
self.__tasks,
@@ -270,29 +272,38 @@ def __run_subtests_asynchronously(self):
self.__separate_build_stdio,
self.__reuse_first_build,
self.__reuse_build_from_id)
- future_to_appname[future] = f'{subtest.getNameOfApplication()}.{subtest.getNameOfSubtest()}'
+ future_to_appname[future] = f'{subtest.getNameOfApplication()}.{subtest.getNameOfSubtest()}'
+ except KeyboardInterrupt:
+ pass
# Log when all job tasks are initiated.
- for my_future in concurrent.futures.as_completed(future_to_appname):
- # appname is appname.testname, as set above
- appname = future_to_appname[my_future]
-
- # Check if an exception has been raised
- my_future_exception = my_future.exception()
- if my_future_exception:
- message = "Test {} exception encountered:\n{}".format(appname, my_future_exception)
- self.__myLogger.doCriticalLogging(message)
-
- subtest_result = my_future.result()
- if subtest_result:
- self.__launched_tests += 1
- message = "Test {} is launched.\n\n".format(appname)
- self.__myLogger.doErrorLogging(message)
- else:
- self.__failed_tests += 1
- self.__failed_test_list.append(appname)
- message = "Test {} failed to launch.\n\n".format(appname)
- self.__myLogger.doErrorLogging(message)
+ all_finished = False
+ while not all_finished:
+ # gracefully handle keyboard interrupts in main thread
+ try:
+ for my_future in concurrent.futures.as_completed(future_to_appname):
+ # appname is appname.testname, as set above
+ appname = future_to_appname[my_future]
+
+ # Check if an exception has been raised
+ my_future_exception = my_future.exception()
+ if my_future_exception:
+ message = "Test {} exception encountered:\n{}".format(appname, my_future_exception)
+ self.__myLogger.doCriticalLogging(message)
+
+ subtest_result = my_future.result()
+ if subtest_result:
+ self.__launched_tests += 1
+ message = "Test {} is launched.\n\n".format(appname)
+ self.__myLogger.doErrorLogging(message)
+ else:
+ self.__failed_tests += 1
+ self.__failed_test_list.append(appname)
+ message = "Test {} failed to launch.\n\n".format(appname)
+ self.__myLogger.doErrorLogging(message)
+ all_finished = True
+ except KeyboardInterrupt:
+ pass
message = "All tests are launched. Yahoo!!"
self.__myLogger.doInfoLogging(message)
diff --git a/harness/libraries/rgt_database_loggers/db_backends/rgt_influxdb.py b/harness/libraries/rgt_database_loggers/db_backends/rgt_influxdb.py
index de8677e7..ed05d60d 100644
--- a/harness/libraries/rgt_database_loggers/db_backends/rgt_influxdb.py
+++ b/harness/libraries/rgt_database_loggers/db_backends/rgt_influxdb.py
@@ -8,6 +8,7 @@
import re
import requests
from urllib.parse import urlparse
+from pathlib import Path
from libraries.rgt_database_loggers.db_backends.base_db import *
@@ -161,7 +162,7 @@ def send_event(self, event_dict : dict):
if event_dict['event_name'] == "build_end":
file_name = os.path.join(event_dict['build_directory'], "output_build.txt")
self.__logger.doDebugLogging(f"Using {file_name} for build output for Influx")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 kb
@@ -172,7 +173,7 @@ def send_event(self, event_dict : dict):
elif event_dict['event_name'] == "submit_end":
file_name = os.path.join(event_dict['run_archive'], "submit.err")
self.__logger.doDebugLogging(f"Using {file_name} for submit errors for Influx")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 kb
@@ -184,7 +185,7 @@ def send_event(self, event_dict : dict):
found_job_file = False
for file_name in glob.glob(event_dict['run_archive'] + "/*.o" + event_dict['job_id']):
self.__logger.doDebugLogging(f"Using {file_name} for job output for Influx")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
found_job_file = True
with open(file_name, "r") as f:
output = f.read()
@@ -196,7 +197,7 @@ def send_event(self, event_dict : dict):
elif event_dict['event_name'] == "check_end":
file_name = os.path.join(event_dict['run_archive'], "output_check.txt")
self.__logger.doDebugLogging(f"Using {file_name} for check output for Influx")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 kb
@@ -280,7 +281,7 @@ def send_node_health_results(self, test_info_dict : dict, node_health_dict : dic
raise DatabaseEnvironmentError("The RGT_NODE_LOCATION_FILE environment variable is required. If you do not want this functionality, please set to \"None\".")
else:
# else, we assume it is a path and we look for it
- if not os.path.exists(os.environ['RGT_NODE_LOCATION_FILE']):
+ if not Path(os.environ['RGT_NODE_LOCATION_FILE']).exists():
raise DatabaseEnvironmentError(f"An RGT_NODE_LOCATION_FILE does not exist at {os.environ['RGT_NODE_LOCATION_FILE']}")
node_locations = {}
diff --git a/harness/libraries/rgt_database_loggers/db_backends/rgt_kafka.py b/harness/libraries/rgt_database_loggers/db_backends/rgt_kafka.py
index 74811ad8..e7289a97 100644
--- a/harness/libraries/rgt_database_loggers/db_backends/rgt_kafka.py
+++ b/harness/libraries/rgt_database_loggers/db_backends/rgt_kafka.py
@@ -9,6 +9,7 @@
import os
import re
import sys
+from pathlib import Path
from confluent_kafka import Producer, KafkaException, KafkaError
from confluent_kafka.admin import AdminClient
@@ -167,7 +168,7 @@ def send_event(self, event_dict : dict):
if event_dict['event_name'] == "build_end":
file_name = os.path.join(event_dict['build_directory'], "output_build.txt")
self.__logger.doDebugLogging(f"Using {file_name} for build output for Kafka")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 KB
@@ -177,7 +178,7 @@ def send_event(self, event_dict : dict):
elif event_dict['event_name'] == "submit_end":
file_name = os.path.join(event_dict['run_archive'], "submit.err")
self.__logger.doDebugLogging(f"Using {file_name} for submit errors for Kafka")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 KB
@@ -188,7 +189,7 @@ def send_event(self, event_dict : dict):
found_job_file = False
for file_name in glob.glob(event_dict['run_archive'] + "/*.o" + event_dict['job_id']):
self.__logger.doDebugLogging(f"Using {file_name} for job output for Kafka")
- if os.path.exists(file_name) and not found_job_file:
+ if Path(file_name).exists() and not found_job_file:
found_job_file = True
with open(file_name, "r") as f:
output = f.read()
@@ -199,7 +200,7 @@ def send_event(self, event_dict : dict):
elif event_dict['event_name'] == "check_end":
file_name = os.path.join(event_dict['run_archive'], "output_check.txt")
self.__logger.doDebugLogging(f"Using {file_name} for check output for Kafka")
- if os.path.exists(file_name):
+ if Path(file_name).exists():
with open(file_name, "r") as f:
output = f.read()
# Truncate to 1 KB
@@ -273,7 +274,7 @@ def send_node_health_results(self, test_info_dict : dict, node_health_dict : dic
raise DatabaseEnvironmentError("The RGT_NODE_LOCATION_FILE environment variable is required. If you do not want this functionality, please set to \"None\".")
else:
# else, we assume it is a path and we look for it
- if not os.path.exists(os.environ['RGT_NODE_LOCATION_FILE']):
+ if not Path(os.environ['RGT_NODE_LOCATION_FILE']).exists():
raise DatabaseEnvironmentError(f"An RGT_NODE_LOCATION_FILE does not exist at {os.environ['RGT_NODE_LOCATION_FILE']}")
node_locations = {}
diff --git a/harness/libraries/rgt_database_loggers/rgt_database_logger.py b/harness/libraries/rgt_database_loggers/rgt_database_logger.py
index 64fba6d0..8d9ee971 100644
--- a/harness/libraries/rgt_database_loggers/rgt_database_logger.py
+++ b/harness/libraries/rgt_database_loggers/rgt_database_logger.py
@@ -6,6 +6,7 @@
"""
import os
+from pathlib import Path
class RgtDatabaseLogger:
@@ -65,7 +66,7 @@ def log_event(self, event_dict : dict, only=None):
if event_dict['event_name'] == 'logging_start':
# Make sure that any explicitly-disabled backends create the dot-file
for dotfile in self.disabled_backends_filenames:
- if not os.path.exists(dotfile):
+ if not Path(dotfile).exists():
os.mknod(dotfile)
num_failed = 0
@@ -78,7 +79,7 @@ def log_event(self, event_dict : dict, only=None):
num_failed += 1
elif event_dict['event_name'] == 'check_end':
# If we just successfully logged check_end, then we add a dot-file to indicate logging completed
- if not os.path.exists(backend.successful_file_name):
+ if not Path(backend.successful_file_name).exists():
os.mknod(backend.successful_file_name)
except Exception as e:
self.logger.doErrorLogging(f"The following exception occurred while logging an event to {backend.url}: {e}.")
@@ -246,7 +247,7 @@ def _check_test_disabled_backend(self, db_logger):
# Check if the environment variables to disable the backend are set
if db_logger.name in self.disabled_backends:
# Create dot-file if it doesn't already exist
- if not os.path.exists(db_logger.disable_file_name):
+ if not Path(db_logger.disable_file_name).exists():
os.mknod(db_logger.disable_file_name)
return True
@@ -254,7 +255,7 @@ def _check_test_disabled_backend(self, db_logger):
# Since the DB backend initialization is NOT done on a per-test basis, it's
# possible that a test previously had InfluxDB disabled, but was not explicitly
# disabled in the current environment. We want to enforce the past disabling
- if os.path.exists(db_logger.disable_file_name):
+ if Path(db_logger.disable_file_name).exists():
self.logger.doDebugLogging(f'Found {db_logger.disable_file_name} in {os.getcwd()}. Disabling {db_logger.name}.')
return True
diff --git a/harness/fundamental_types/rgt_state.py b/harness/libraries/rgt_state.py
similarity index 100%
rename from harness/fundamental_types/rgt_state.py
rename to harness/libraries/rgt_state.py
diff --git a/harness/machine_types/rgt_test.py b/harness/libraries/rgt_test.py
similarity index 76%
rename from harness/machine_types/rgt_test.py
rename to harness/libraries/rgt_test.py
index ae432f7b..5b4ea152 100644
--- a/harness/machine_types/rgt_test.py
+++ b/harness/libraries/rgt_test.py
@@ -29,16 +29,6 @@
The [EnvVars] section is optional
The section contains keys-value pairs for setting environmental varibles.
-The [RuntimeEnvironmentCommands] section optional. This section contains
-key-value entries for where the values ar commands to be run that set the
-runtime environment for each harness task.
-The only permmited keys are
-
- * "build_rte_cmd" - key for the command to set the rte for the build task.
- * "submit_rte_cmd" - key for the command to set the rte for the submit task.
- * "check_rte_cmd" - key for the command to set the rte for the check task.
- * "report_rte_cmd" - key for the command to set the rte for the report task.
- * "all_rte_cmd" - key for the command to set the rte for the all tasks.
"""
#
@@ -49,22 +39,22 @@
import configparser
import os
import sys
+from pathlib import Path
+try:
+ # YAML & Jinja2 must be used together
+ import yaml
+ from jinja2 import Template
+ yaml_disabled = False
+except ModuleNotFoundError:
+ yaml_disabled = True
# Harness imports
from libraries.rgt_utilities import rgt_variable_name_modification
from libraries import rgt_utilities
-
class RgtTest():
"""This class is the abstraction of regression test input file."""
- RUNTIME_ENVIRONMENT_SECTION_KEYS = {"build" : 'build_rte_cmd',
- "submit" : 'submit_rte_cmd',
- "check" : 'check_rte_cmd',
- "report" : 'report_rte_cmd',
- "all" : 'all_rte_cmd'}
- """Valid key values for the runtime environment section in the rgt_test_input.ini file."""
-
HARNESS_SECTION_KEYS = {"application_test_results_dir" : 'results_dir',
"application_test_work_dir" : 'working_dir',
"application_test_build_dir" : 'build_dir',
@@ -74,7 +64,7 @@ class RgtTest():
OBTAIN_FROM_ENVIRONMENT=""
- """str: The string value for an INI entry that indicates to get the value from the shell environment."""
+ """str: The string value for an entry that indicates to get the value from the shell environment."""
def __init__(self, filename,logger=None):
""" The constructor of the RgtTest class.
@@ -113,17 +103,6 @@ def __init__(self, filename,logger=None):
input file.
"""
- self._runtime_environment_params = {}
- """ A dictionary: A dictionary of commands to set the runtime environment.
-
- The keys of the dictionary are strings, and the corrsponding values
- specify a command. See the class variable RUNTIME_ENVIRONMENT_KEYS
- for valid keys.
-
- For example, self._runtime_environment_params['build_rte_cmd'] is
- the command to set the runtime environment for building the binary.
- """
-
self._harness_params = {}
"""A dictionary: A dictionary of keys and values needed by the harness
@@ -133,22 +112,17 @@ def __init__(self, filename,logger=None):
# dict of builtin keys - value indicates whether it is required
self.__builtin_keys = {
-
"batch_filename" : {"required": True, "type": str },
"batch_queue" : {"required": False, "type": str },
"build_cmd" : {"required": True, "type": str},
"check_cmd": {"required": True, "type": str},
- "executable_path" : {"required": False, "type": str},
"job_name" : {"required": True, "type": str},
"max_submissions" : {"required": False, "type": int, "valid": lambda x : True if (int(x) >= 1 or int(x) == -1) else False},
"nodes" : {"required": True, "type": int, "valid": lambda x: True if (int(x) >= 1) else False},
- "processes_per_node" : {"required": False, "type": int, "valid": lambda x: True if (int(x) >= 1) else False},
"project_id" : {"required": False, "type": str},
- "report_cmd" : {"required": True, "type": str},
+ "report_cmd" : {"required": False, "type": str},
"resubmit" : {"required": False, "type": int, "valid": lambda x: True if (int(x) == 1 or int(x) == 0) else False},
- "total_processes" : {"required": False, "type": int, "valid": lambda x: True if (int(x) >= 1) else False},
- "use_batch_template": {"required": False, "type": int, "valid": lambda x: True if (int(x) == 1 or int(x) == 0) else False},
- "walltime" : {"required": True, "type": str},
+ "use_batch_template": {"required": False, "type": int, "valid": lambda x: True if (int(x) == 1 or int(x) == 0) else False}
}
def __str__(self):
@@ -206,56 +180,6 @@ def print_user_parameters(self):
for (k,v) in (self.user_parameters).items():
self.__logger.doInfoLogging(f'{k}={v}')
- # Methods to manage runtime environment commands
- @property
- def runtime_environment_params(self):
- """dict: The dictionary of key-values for setting the runtime environment commands."""
- return self._runtime_environment_params
-
- @runtime_environment_params.setter
- def runtime_environment_params(self,params):
- """Sets the commands for the setting various runtime environment commands.
-
- Parameters
- ----------
- params
- A dictionary where the keys and values are strings.
- """
- for (key,val) in params.items():
- if key in self.RUNTIME_ENVIRONMENT_SECTION_KEYS.values():
- self._runtime_environment_params[key] = val
- else:
- # To do is throw an exception if an invalid key,value is assigned.
- pass
-
- @property
- def build_runtime_environment_command_file(self):
- """str: The command file to set the runtime environment for building the binary."""
- key = self.RUNTIME_ENVIRONMENT_SECTION_KEYS["build"]
- command = self._get_rte_param(key)
- return command
-
- @property
- def submit_runtime_environment_command_file(self):
- """str: The command file to set the runtime environment for submitting the batch script."""
- key = self.RUNTIME_ENVIRONMENT_SECTION_KEYS["submit"]
- command = self._get_rte_param(key)
- return command
-
- @property
- def check_runtime_environment_command_file(self):
- """str: The command file to set the runtime environment for checking the test results."""
- key = self.RUNTIME_ENVIRONMENT_SECTION_KEYS["check"]
- command = self._get_rte_param(key)
- return command
-
- @property
- def report_runtime_environment_command_file(self):
- """str: The command file to set the runtime environment for reporting the test results."""
- key = self.RUNTIME_ENVIRONMENT_SECTION_KEYS["report"]
- command = self._get_rte_param(key)
- return command
-
#
# Methods to retrieve full test dictionaries
#
@@ -339,18 +263,19 @@ def get_test_replacements(self):
key found in the Replacements section of application-test input file
rgt_test_input.ini.
"""
+ def name_mangle(yaml_origin, name):
+ return name if yaml_origin else f'__{name}__'
+
replacements = {}
+ is_yaml = self.__inputfile.endswith('yaml')
for (k,v) in (self.builtin_parameters).items():
- replace_key = '__' + k + '__'
- replacements[replace_key] = v
+ replacements[name_mangle(is_yaml, k)] = v
for (k,v) in (self.user_parameters).items():
- replace_key = '__' + k + '__'
- replacements[replace_key] = v
+ replacements[name_mangle(is_yaml, k)] = v
for (k,v) in (self.harness_parameters).items():
- replace_key = '__' + k + '__'
- replacements[replace_key] = v
+ replacements[name_mangle(is_yaml, k)] = v
return replacements
@@ -379,41 +304,21 @@ def get_check_command(self):
def get_report_command(self):
return self._get_builtin_param("report_cmd")
- def get_executable(self):
- return self._get_builtin_param("executable_path")
-
def get_jobname(self):
return self._get_builtin_param("job_name")
def get_max_submissions(self):
return self._get_builtin_param("max_submissions")
+ def get_use_batch_template(self):
+ return self._get_builtin_param("use_batch_template")
+
def get_nodes(self):
return self._get_builtin_param("nodes")
def get_project(self):
return self._get_builtin_param("project_id")
- def get_use_batch_template(self):
- return self._get_builtin_param("use_batch_template")
-
- def get_walltime(self):
- return self._get_builtin_param("walltime")
-
- def get_total_processes(self):
- val = self._get_builtin_param("total_processes")
- if not val:
- return str(0)
- else:
- return val
-
- def get_processes_per_node(self):
- val = self._get_builtin_param("processes_per_node")
- if not val:
- return str(0)
- else:
- return val
-
#
# Input file readers
#
@@ -425,8 +330,17 @@ def read_input_file(self):
is not a permitted value.
"""
try:
- if os.path.isfile(self.test_input_filename):
- self._read_rgt_input_ini()
+ if Path(self.test_input_filename).is_file():
+ if self.test_input_filename.endswith('ini'):
+ self._read_rgt_input_ini()
+ elif self.test_input_filename.endswith('yaml'):
+ if yaml_disabled:
+ self.__logger.doCriticalLogging("import yaml failed, YAML test input file cannot be loaded. Please pip install pyyaml in the current Python environment.")
+ exit(1)
+ self._read_rgt_input_yaml()
+ else:
+ error_message = "File type of input file {} not supported (expected yaml or ini).".format(self.test_input_filename)
+ raise ErrorRgtTestInputFileNotFound(error_message)
self._reconcile_with_shell_environment_variables()
self._check_parameters()
self._print_test_parameters()
@@ -451,12 +365,6 @@ def _get_builtin_param(self, key):
def _is_builtin_param(self, key):
return key in self.__builtin_keys
- def _get_rte_param(self,key):
- command = ""
- if key in self.runtime_environment_params:
- command = self.runtime_environment_params[key]
- return command
-
def _set_builtin_param(self, key, val, warn=True):
if self._is_builtin_param(key):
self.__builtin_params[key] = val
@@ -466,9 +374,6 @@ def _set_builtin_param(self, key, val, warn=True):
self.__logger.doWarningLogging("WARNING: Ignoring invalid built-in parameter key {}".format(key))
return False
- def _is_rte_param(self,key):
- return key in self.RUNTIME_ENVIRONMENT_SECTION_KEYS.values()
-
def _update_replacement_parameters(self,params_view):
"""Updates the appropiate replacement parameter dictionary as required."""
for (k,v) in params_view:
@@ -482,8 +387,7 @@ def _read_rgt_input_ini(self):
rgt_test_config.read(self.test_input_filename)
if not 'Replacements' in rgt_test_config:
- self.__logger.doCriticalLogging("Missing [Replacements] section in test input")
- replace = dict()
+ raise Exception("Missing [Replacements] section in test input")
else:
replace = rgt_test_config['Replacements']
self._update_replacement_parameters(replace.items())
@@ -502,13 +406,40 @@ def _read_rgt_input_ini(self):
env_vars = rgt_test_config['EnvVars']
self.test_environment = env_vars
- # We now extract the runtime environment commands.
- rte_section = 'RuntimeEnvironmentCommands'
- if rte_section in rgt_test_config:
- runtime_env_commands = rgt_test_config[rte_section]
+ def _read_rgt_input_yaml(self):
+ with open(self.test_input_filename, 'r') as file:
+ test_yaml_raw = yaml.safe_load(file)
+
+ # Catch a few fatal errors and throw exceptions if encountered
+ if not 'replacements' in test_yaml_raw.keys():
+ raise Exception("Missing Replacements section in YAML test input")
+ elif 'variables' in test_yaml_raw.keys() and not isinstance(test_yaml_raw["variables"], dict):
+ # variables must be a single key-value dict, not a list of dicts
+ raise Exception("Variables are provided in the YAML test input, but is not a dictionary")
+
+ if 'variables' in test_yaml_raw.keys():
+ # then do string formatting only for string data types
+ rgt_test_config = { k: v.format(**test_yaml_raw["variables"]) if isinstance(v, str) else v
+ for k, v in test_yaml_raw["replacements"].items() }
else:
- runtime_env_commands = dict()
- self.runtime_environment_params = runtime_env_commands
+ # then no variable usage, just copy replacements block to test config
+ rgt_test_config = test_yaml_raw["replacements"]
+
+ self._update_replacement_parameters(rgt_test_config.items())
+
+ # Update environment if either batch_queue or project_id is set
+ env_dict = {}
+ bq = self.get_batch_queue()
+ if bq:
+ env_dict['batch_queue'] = bq
+ proj = self.get_project()
+ if proj:
+ env_dict['project_id'] = proj
+ rgt_utilities.set_harness_environment(env_dict, override=True)
+
+ # EnvVars and RuntimeEnvironmentParams are not supported in YAML format
+ self.test_environment = dict()
+ self.runtime_environment_params = dict()
def _print_test_parameters(self):
self._print_builtin_parameters()
@@ -567,14 +498,15 @@ def _check_parameters(self):
if 'type' in params and k in self.builtin_parameters:
# All params are strings, so no need to test that
# Check int
- if params['type'] is int and not self.builtin_parameters[k].lstrip("-").isdigit():
+ if params['type'] is int and not (isinstance(self.builtin_parameters[k],int) or \
+ self.builtin_parameters[k].lstrip("-").isdigit()):
valid_type = False # Need to reference in lambda function
error_message += "ERROR: test input parameter {} is not type {}!\n".format(k, str(params['type']))
# Check file
if params['type'] == 'file':
# Check whether it exists
- if not os.path.exists(self.builtin_parameters[k]):
+ if not Path(self.builtin_parameters[k]).exists():
error_message += "ERROR: test input parameter {} does not exist {}!\n".format(k, self.builtin_parameters[k])
# Check whether is executable
diff --git a/harness/libraries/status_file.py b/harness/libraries/status_file.py
index 7a635618..ec3c32a2 100644
--- a/harness/libraries/status_file.py
+++ b/harness/libraries/status_file.py
@@ -19,6 +19,7 @@
import urllib
import dateutil.parser
import subprocess
+from pathlib import Path
from libraries.layout_of_apps_directory import apptest_layout
from libraries.rgt_database_loggers.rgt_database_logger_factory import create_rgt_db_logger
@@ -538,7 +539,7 @@ def __log_event(self, event_id, event_filename, event_type, event_subtype,
dir_head = os.path.split(os.getcwd())[0]
file_path = os.path.join(dir_head, apptest_layout.test_status_dirname, str(self.__test_id),
event_filename)
- if os.path.exists(file_path):
+ if Path(file_path).exists():
self.__logger.doWarningLogging('Warning: event log file already exists. ' + file_path)
file_path_partial = os.path.join(dir_head, apptest_layout.test_status_dirname,
@@ -581,7 +582,7 @@ def __log_event(self, event_id, event_filename, event_type, event_subtype,
def __create_status_file(self,path_to_status_file):
"""Create the status file for this app/test if it doesn't exist."""
- if not os.path.exists(path_to_status_file):
+ if not Path(path_to_status_file).exists():
with open(self.__status_file_path, "w") as file_obj :
file_obj.write(StatusFile.header)
@@ -744,7 +745,7 @@ def get_status_info(test_id, event_type, event_subtype,
test_instance_info['path_to_rgt_package'] = (
os.environ['PATH_TO_RGT_PACKAGE']
- if 'PATH_TO_RGT_PACKAGE' in os.environ else no_value)
+ if 'PATH_TO_RGT_PACKAGE' in os.environ else str(Path(__file__).resolve().parent.parent))
test_instance_info['rgt_system_log_tag'] = (
os.environ['RGT_SYSTEM_LOG_TAG']
@@ -763,7 +764,7 @@ def get_status_info(test_id, event_type, event_subtype,
event_info['runtag'] = test_instance_info['rgt_system_log_tag']
file_check_alias = os.path.join(run_archive_all, test_id, 'check_alias.txt')
- if os.path.exists(file_check_alias):
+ if Path(file_check_alias).exists():
file_ = open(file_check_alias, 'r')
check_alias_ = file_.read()
file_.close()
@@ -772,7 +773,7 @@ def get_status_info(test_id, event_type, event_subtype,
event_info['check_alias'] = no_value
file_job_id = os.path.join(dir_status_this_test, apptest_layout.job_id_filename)
- if os.path.exists(file_job_id):
+ if Path(file_job_id).exists():
file_ = open(file_job_id, 'r')
job_id_ = file_.read()
file_.close()
@@ -781,7 +782,7 @@ def get_status_info(test_id, event_type, event_subtype,
event_info['job_id'] = no_value
file_job_status = os.path.join(dir_status_this_test, apptest_layout.job_status_filename)
- if os.path.exists(file_job_status):
+ if Path(file_job_status).exists():
file_ = open(file_job_status, 'r')
job_status_ = file_.read()
file_.close()
@@ -818,7 +819,7 @@ def get_status_info_from_file(event_filename):
event_info = {}
- if not os.path.exists(event_filename):
+ if not Path(event_filename).exists():
event_info['text'] = f'Could not find file {event_filename}'
return event_info
file_ = open(event_filename, 'r')
@@ -847,7 +848,7 @@ def write_system_log(test_id, status_info):
is_using_unix_logger = False
if rgt_system_log_dir == '':
is_using_unix_logger = True
- elif not os.path.exists(rgt_system_log_dir):
+ elif not Path(rgt_system_log_dir).exists():
is_using_unix_logger = True
rgt_system_log_tag = (os.environ['RGT_SYSTEM_LOG_TAG']
@@ -907,7 +908,7 @@ def parse_status_file(path_to_status_file, startdate, enddate,
failed_jobs = []
- if os.path.exists(path_to_status_file):
+ if Path(path_to_status_file).exists():
pass
else:
return shash
@@ -976,7 +977,7 @@ def parse_status_file2(path_to_status_file):
failed_jobs = []
- if not os.path.exists(path_to_status_file):
+ if not Path(path_to_status_file).exists():
return shash
sfile_obj = open(path_to_status_file, 'r')
diff --git a/harness/machine_types/__init__.py b/harness/machine_types/__init__.py
index 591f2ced..5330b4f1 100644
--- a/harness/machine_types/__init__.py
+++ b/harness/machine_types/__init__.py
@@ -1,6 +1,4 @@
__all__ = ["machine_factory",
"machine_factory_exceptions",
- "ibm_power9",
"linux_x86_64",
- "linux_utilities",
- "rgt_test"]
+ "linux_utilities"]
diff --git a/harness/machine_types/base_machine.py b/harness/machine_types/base_machine.py
index 8e09fda1..0ac2516a 100644
--- a/harness/machine_types/base_machine.py
+++ b/harness/machine_types/base_machine.py
@@ -7,16 +7,16 @@
# Python imports
from abc import abstractmethod, ABCMeta
-from pathlib import Path
import os
import shutil
import subprocess
import shlex
import sys
+from pathlib import Path
# Harness imports
from libraries.apptest import subtest
-from .scheduler_factory import SchedulerFactory
+from schedulers.scheduler_factory import SchedulerFactory
from machine_types import linux_utilities
class BaseMachine(metaclass=ABCMeta):
@@ -44,11 +44,11 @@ class BaseMachine(metaclass=ABCMeta):
# The constructor of class base_machine.
def __init__(self, name, scheduler_type,
numNodes, numSockets, numCoresPerSocket,
- apptest, separate_build_stdio=False):
+ apptest, separate_build_stdio=False, use_jinja2=False):
self.__name = name
- self.__scheduler = SchedulerFactory.create_scheduler(scheduler_type, logger=apptest.logger)
+ self.__scheduler = SchedulerFactory.create_scheduler(scheduler_type, logger=apptest.logger, use_jinja2=use_jinja2)
"""An object of type BaseScheduler : This object is the job resource scheduler. See the
classs SchedulerFactory for more details."""
@@ -106,21 +106,6 @@ def check_command(self):
def test_config(self):
return
- @property
- @abstractmethod
- def build_runtime_environment_command_file(self):
- return
-
- @property
- @abstractmethod
- def submit_runtime_environment_command_file(self):
- return
-
- @property
- @abstractmethod
- def check_runtime_environment_command_file(self):
- return
-
def isTestCycleComplete(self,stest):
"""Checks if the subtest has completed its cycle.
Parameters
@@ -187,19 +172,7 @@ def submit_batch_script(self):
message = f"The initial directory is {currentdir}"
self.logger.doInfoLogging(message)
- # Get the environment using the submit runtime environment file.
new_env = None
- filename = self.submit_runtime_environment_command_file
-
- try:
- if filename != "":
- message = f"The submit runtime environmental file is {filename}."
- self.logger.doInfoLogging(message)
- new_env = linux_utilities.get_new_environment(self,filename)
- except SetBuildRTEError as error:
- message = f"Unable to set the submit runtime environment."
- self.logger.doCriticalLogging(message)
-
exit_status = linux_utilities.submit_batch_script(self,new_env)
if exit_status != 0:
@@ -287,7 +260,7 @@ def build_executable(self):
self.logger.doErrorLogging(f"Path to Run_Archive: {path_to_runarchive_directory}")
if 'RGT_REUSE_BUILD_FROM' in os.environ and \
- os.path.exists(os.environ['RGT_REUSE_BUILD_FROM']):
+ Path(os.environ['RGT_REUSE_BUILD_FROM']).exists():
self.logger.doInfoLogging(f"Skipping build, re-using the build from {os.environ['RGT_REUSE_BUILD_FROM']}")
return 0
@@ -299,17 +272,6 @@ def build_executable(self):
self.logger.doInfoLogging(f"Copied source to build directory.")
- # Get the environment using the build runtime environment file.
- new_env = None
- filename = self.build_runtime_environment_command_file
-
- if filename != "":
- self.logger.doInfoLogging(f"The build runtime environmental file is {filename}.")
- new_env = linux_utilities.get_new_environment(self,filename)
- message = f"The new build environment is as follows:\n"
- message += str(new_env)
- self.logger.doInfoLogging(message)
-
# We now change directories to the build directory.
os.chdir(path_to_build_directory)
@@ -317,6 +279,7 @@ def build_executable(self):
self.logger.doInfoLogging(message)
# We run the build command.
+ new_env = None
exit_status = self._build_executable(new_env)
message = f"The build exit status is {exit_status}."
@@ -347,17 +310,6 @@ def check_executable(self):
currentdir = os.getcwd()
runarchive_dir = self.apptest.get_path_to_runarchive()
- # Get the environment using the check runtime environment file.
- new_env = None
- filename = self.check_runtime_environment_command_file
- try:
- if filename != "":
- message = f"The check runtime environmental file is {filename}."
- new_env = linux_utilities.get_new_environment(self,filename)
- except SetBuildRTEError as error:
- message = f"Unable to set the check runtime environment."
- self.logger.doCriticalLogging(message)
-
# We now change to the runarchive directory.
os.chdir(runarchive_dir)
@@ -365,6 +317,7 @@ def check_executable(self):
self.logger.doInfoLogging(message)
# We now run the check command.
+ new_env = None
check_status = linux_utilities.check_executable(self,new_env)
self._write_check_exit_status(check_status)
@@ -386,6 +339,10 @@ def start_report_executable(self):
"""
report_command_str = self.test_config.get_report_command()
+ if not report_command_str:
+ self.logger.doInfoLogging("No report command provided, skipping report step.")
+ return 0
+
message = f"Running report executable script report script {report_command_str }."
print(message)
@@ -439,7 +396,7 @@ def _copy_source_to_build_directory(self):
path_to_build_directory = self.apptest.get_path_to_workspace_build()
if 'RGT_REUSE_BUILD_FROM' in os.environ and \
- os.path.exists(os.environ['RGT_REUSE_BUILD_FROM']):
+ Path(os.environ['RGT_REUSE_BUILD_FROM']).exists():
self.logger.doInfoLogging("RGT_REUSE_BUILD_FROM set, skipping copying Source.")
return 0
@@ -447,7 +404,7 @@ def _copy_source_to_build_directory(self):
dst=path_to_build_directory,
symlinks=True)
# If a Source directory exists inside test, overlay that over source directory
- if os.path.exists(path_to_test_source):
+ if Path(path_to_test_source).exists():
# Python 3.8 adds the dirs_exist_ok keyword to allow overwriting a destination
# Prior to that, it's easier to use shell commands to do what we want
if sys.version_info[0] == 3 and sys.version_info[1] >= 8:
@@ -511,22 +468,5 @@ class BaseMachineError(Exception):
"""Base class for exceptions in this module"""
pass
-class SetBuildRTEError(BaseMachineError):
- """Exception raised for errors in setting the build runtime environment."""
- def __init__(self,message):
- """The class constructor
-
- Parameters
- ----------
- message : string
- The error message for this exception.
- """
- self._message = message
-
- @property
- def message(self):
- """str: The error message."""
- return self._message
-
if __name__ == "__main__":
print("This is the BaseMachine class!")
diff --git a/harness/machine_types/ibm_power9.py b/harness/machine_types/ibm_power9.py
deleted file mode 100644
index 6a72eae7..00000000
--- a/harness/machine_types/ibm_power9.py
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env python3
-
-# %Authors%
-
-# Python imports.
-import sys
-import os
-import shlex
-import subprocess
-import time
-import re
-
-# Local imports.
-from machine_types.base_machine import BaseMachine
-from machine_types.rgt_test import RgtTest
-
-class IBMpower9(BaseMachine):
-
- def __init__(self,
- name='IBM Power9',
- scheduler=None,
- numNodes=1,
- numSocketsPerNode=2,
- numCoresPerSocket=21,
- apptest=None,
- separate_build_stdio=False):
-
- BaseMachine.__init__(self,
- name=name,
- scheduler_type=scheduler,
- numNodes = numNodes,
- numSockets = numSocketsPerNode,
- numCoresPerSocket = numCoresPerSocket,
- apptest=apptest,
- separate_build_stdio=separate_build_stdio)
-
- # process test input file. The subtest knows the path to the
- # the test input file.
- path_to_test_input_file = apptest.path_of_test_input_file
- self._rgt_test = RgtTest(path_to_test_input_file,logger=self.logger)
- self._rgt_test.read_input_file()
-
- # Add test parameters needed by the harness
- harness_parameters = {}
- harness_parameters['results_dir'] = self.apptest.get_path_to_runarchive()
- harness_parameters['working_dir'] = self.apptest.get_path_to_workspace_run()
- harness_parameters['build_dir'] = self.apptest.get_path_to_workspace_build()
- harness_parameters['scripts_dir'] = self.apptest.get_path_to_scripts()
- harness_parameters['harness_id'] = self.apptest.get_harness_id()
- self._rgt_test.harness_parameters.update(harness_parameters)
-
- @property
- def build_runtime_environment_command_file(self):
- return self.test_config.build_runtime_environment_command_file
-
- @property
- def submit_runtime_environment_command_file(self):
- return self.test_config.submit_runtime_environment_command_file
-
- @property
- def check_runtime_environment_command_file(self):
- return self.test_config.check_runtime_environment_command_file
-
- @property
- def test_config(self):
- return self._rgt_test
-
- #-----------------------------------------------------
- # -
- # Private methods -
- # -
- #-----------------------------------------------------
-
-if __name__ == "__main__":
- print('This is the IBM Power9 class')
diff --git a/harness/machine_types/linux_utilities.py b/harness/machine_types/linux_utilities.py
index eb84b41d..b70ab561 100644
--- a/harness/machine_types/linux_utilities.py
+++ b/harness/machine_types/linux_utilities.py
@@ -13,6 +13,14 @@
import shlex
import time
+from pathlib import Path
+
+try:
+ from jinja2 import Template, TemplateError
+except ImportError:
+ pass
+
+
class LinuxEnvRegxp:
"""
When one does an env | less on Linux, we get results similar to the following:
@@ -87,39 +95,74 @@ def make_batch_script_for_linux(a_machine):
message = f"The batch scheduler template file is {batch_template_file}."
a_machine.logger.doInfoLogging(message)
- # Get batch job template lines
- try :
- with open(batch_template_file, "r") as templatefileobj:
- templatelines = templatefileobj.readlines()
- except OSError as err:
- bstatus = False
- message = ( f"Error opening batch template file '{batch_template_file}' for reading.\n"
- f"Handling error: {err}\n" )
- a_machine.logger.doCriticalLogging(message)
-
- if bstatus:
- message = f"Completed reading lines of the batch template file {batch_template_file}."
- a_machine.logger.doInfoLogging(message)
-
- # Create test batch job script in run archive directory
+ if batch_template_file.endswith('x'):
+ # Get batch job template lines
try :
- with open(batch_file_path, "w") as batch_job:
- # Replace all the wildcards in the batch job template with the values in
- # the test config
- test_replacements = a_machine.test_config.get_test_replacements()
- for record in templatelines:
- for (replace_key,val) in test_replacements.items():
- re_tmp = re.compile(replace_key)
- record = re_tmp.sub(val, record)
- batch_job.write(record)
+ with open(batch_template_file, "r") as templatefileobj:
+ templatelines = templatefileobj.readlines()
except OSError as err:
bstatus = False
- message = ( f"Error opening batch template file '{batch_file_path}' for writing.\n"
+ message = ( f"Error opening batch template file '{batch_template_file}' for reading.\n"
f"Handling error: {err}\n" )
a_machine.logger.doCriticalLogging(message)
+
+ if bstatus:
+ message = f"Completed reading lines of the batch template file {batch_template_file}."
+ a_machine.logger.doInfoLogging(message)
+
+ # Create test batch job script in run archive directory
+ try :
+ with open(batch_file_path, "w") as batch_job:
+ # Replace all the wildcards in the batch job template with the values in
+ # the test config
+ test_replacements = a_machine.test_config.get_test_replacements()
+ for record in templatelines:
+ for (replace_key,val) in test_replacements.items():
+ re_tmp = re.compile(replace_key)
+ record = re_tmp.sub(val, record)
+ batch_job.write(record)
+ except OSError as err:
+ bstatus = False
+ message = ( f"Error opening batch template file '{batch_file_path}' for writing.\n"
+ f"Handling error: {err}\n" )
+ a_machine.logger.doCriticalLogging(message)
+
+ message = f"Completed regex substitutions."
+ a_machine.logger.doInfoLogging(message)
+ elif batch_template_file.endswith('j2'):
+ try:
+ tpl_text = Path(batch_template_file).read_text(encoding="utf-8")
+ repl_dict = a_machine.test_config.get_test_replacements()
+ rendered = Template(tpl_text).render(**repl_dict)
+ Path(batch_file_path).write_text(rendered, encoding="utf-8")
+ bstatus = True
+ except FileNotFoundError as e:
+ a_machine.logger.doCriticalLogging(f"Error: template file not found: {e.filename}")
+ bstatus = False
+ pass
+ except PermissionError as e:
+ a_machine.logger.doCriticalLogging(f"Error: permission denied accessing '{e.filename}'")
+ bstatus = False
+ pass
+ except TemplateError as e:
+ a_machine.logger.doCriticalLogging(f"Error: Jinja2 template/rendering failed: {e}")
+ bstatus = False
+ pass
+ except OSError as e:
+ a_machine.logger.doCriticalLogging(f"Error: I/O error while reading/writing files: {e}")
+ bstatus = False
+ pass
+ except Exception as e:
+ a_machine.logger.doCriticalLogging(f"Error: unexpected failure: {e}")
+ bstatus = False
+ pass
+
message = f"Completed regex substitutions."
a_machine.logger.doInfoLogging(message)
+ else:
+ bstatus = False
+ a_machine.logger.doCriticalLogging(f"Batch template file has unknown extension: {batch_template_file}.")
return bstatus
@@ -240,134 +283,6 @@ def isTestCycleComplete(stest):
return subtest_cyle_complete
-def get_new_environment(a_machine,filename):
- """ Returns a dictionary of the environmental variables.
-
- The method returns a dictionary of the environment of a process that
- runs the command to set the build runtime environment. The command
- along with the env command is writen to random file. The random file is
- executed and the output is captured parsed into a dictionary.
-
- Parameters
- ----------
- filename : str
- The name of the file that contains the command to set the environment.
-
- Returns
- -------
- dict
- A dictionary obj["env_key"] = env_value where env_key is the environmental
- variable and env_value is its value.
- """
- path_to_build_directory = a_machine.apptest.get_path_to_workspace_build()
- tmp_source_file = os.path.join(path_to_build_directory,"tmp_source_file")
- std_out_file = os.path.join(path_to_build_directory,"std.env.out.txt")
- std_err_file = os.path.join(path_to_build_directory,"std.env.err.txt")
-
- #-----------------------------------------------------
- # Write the current environmental variables to file. -
- # -
- #-----------------------------------------------------
- with open(tmp_source_file, 'w') as tmp_src_file:
- tmp_src_file.write('#!/usr/bin/env bash\n')
- tmp_src_file.write('source %s\n'%filename)
- tmp_src_file.write('env\n')
-
- # Execute the random file with Popen and capture the std output.
- os.chmod(tmp_source_file,0o755)
- with open(std_out_file, 'w') as out:
- with open(std_err_file, 'w') as err:
- with subprocess.Popen([tmp_source_file],
- shell=False,
- cwd=path_to_build_directory,
- stdout=out, stderr=err) as process1:
- process1.wait()
-
- if process1.returncode != 0:
- message = "The return code of the Popen process to set the environment != 0."
- raise BaseMachine.SetBuildRTEError(message)
-
- #-----------------------------------------------------
- # Read the file and store the in list records. -
- # -
- #-----------------------------------------------------
- with open(std_out_file, 'r') as infile:
- records = infile.readlines()
-
- #-----------------------------------------------------
- # Now loop over the records and process -
- # the environment variables. -
- # -
- #-----------------------------------------------------
- env_dict = {}
- current_line_nm = 0
- nm_records = len(records)
- while current_line_nm < nm_records:
- # Check that on the current line we have a new environmental
- # variable entry for this line. If a new environmental variable
- # is not found then proceed to the next line.
- record_decoded = records[current_line_nm]
-
- search = LinuxEnvRegxp.env_variable_regxp.search(record_decoded)
- if search:
- key=search.group('key')
- a_machine.logger.doInfoLogging(f"Found new env variable {key} at line: {current_line_nm}")
- else:
- message = "Error in finding the next environment variable.\n"
- message += f"The following line, #{current_line_nm}, had no matches for searches:\n"
- message += record_decoded + "\n"
- a_machine.logger.doCriticalLogging(message)
- raise BaseMachine.SetBuildRTEError(message)
-
- # We now get the range of entries for this environmental variable.
- start_line = current_line_nm
-
- # Set the pending current line number to the current
- # line number.
- pending_current_line_nm = current_line_nm
-
- if ( start_line == (nm_records-1) ):
-
- # We are at the last line and the finish
- # line is the last line.
- finish_line = nm_records - 1
-
- pending_current_line_nm += 1
- else:
-
- search_range_begin = current_line_nm + 1 # Set the start range for
- # searching the next environmental entry
-
- max_search_range_end = nm_records - 1 # Set the maximum rnage of lines to search.
- # Offset by 1 because records
- # list index starts at 0.
-
- a_machine.logger.doInfoLogging(f"Search range is {search_range_begin} to {max_search_range_end}.")
-
- for tmp_line_nm in range(search_range_begin,max_search_range_end+1,1):
- pending_current_line_nm += 1
- record_decoded = records[tmp_line_nm]
- search = LinuxEnvRegxp.env_variable_regxp.search(record_decoded)
- if search:
- # We have found the next environmental variable entry
- # so break from for loop.
- break
-
-
- # The finish_line is 1 less than the pending_current_line_nm
- # due to the prior for loop breaking at the start of the
- # next environmental variable.
- finish_line = pending_current_line_nm - 1
-
- # We now parse the range of entries for the environmental key and
- # value.
- _parse_env_variable(records[start_line:(finish_line+1)],env_dict)
-
- # The current line now is now equal to pending_current_line_nm.
- current_line_nm = pending_current_line_nm
-
- return env_dict
-
def build_executable(a_machine, new_env):
""" Return the status of the build. Runs the build command.
diff --git a/harness/machine_types/linux_x86_64.py b/harness/machine_types/linux_x86_64.py
index 7d53bb36..93356b54 100644
--- a/harness/machine_types/linux_x86_64.py
+++ b/harness/machine_types/linux_x86_64.py
@@ -9,10 +9,11 @@
import subprocess
import time
import re
+from pathlib import Path
# Local imports.
from machine_types.base_machine import BaseMachine
-from machine_types.rgt_test import RgtTest
+from libraries.rgt_test import RgtTest
class Linux_x86_64(BaseMachine):
@@ -25,6 +26,16 @@ def __init__(self,
apptest=None,
separate_build_stdio=False):
+ # process test input file. The subtest knows the path to the
+ # the test input file.
+ path_to_test_input_file = apptest.path_of_test_input_file_ini
+ using_yaml = False
+ # if ini does not exist, try yaml
+ if not Path(path_to_test_input_file).is_file():
+ path_to_test_input_file = apptest.path_of_test_input_file_yaml
+ using_yaml = True
+
+ # Now tell the base machine if it needs a jinja template or not
BaseMachine.__init__(self,
name=name,
scheduler_type=scheduler,
@@ -32,11 +43,9 @@ def __init__(self,
numSockets=numSocketsPerNode,
numCoresPerSocket=numCoresPerSocket,
apptest=apptest,
- separate_build_stdio=separate_build_stdio)
+ separate_build_stdio=separate_build_stdio,
+ use_jinja2=using_yaml)
- # process test input file. The subtest knows the path to the
- # the test input file.
- path_to_test_input_file = apptest.path_of_test_input_file
self._rgt_test = RgtTest(path_to_test_input_file,logger=self.logger)
self._rgt_test.read_input_file()
@@ -49,18 +58,6 @@ def __init__(self,
harness_parameters['harness_id'] = self.apptest.get_harness_id()
self._rgt_test.harness_parameters.update(harness_parameters)
- @property
- def build_runtime_environment_command_file(self):
- return self.test_config.build_runtime_environment_command_file
-
- @property
- def submit_runtime_environment_command_file(self):
- return self.test_config.submit_runtime_environment_command_file
-
- @property
- def check_runtime_environment_command_file(self):
- return self.test_config.check_runtime_environment_command_file
-
@property
def test_config(self):
return self._rgt_test
diff --git a/harness/machine_types/machine_factory.py b/harness/machine_types/machine_factory.py
index 795eeb7b..6dd06eb2 100644
--- a/harness/machine_types/machine_factory.py
+++ b/harness/machine_types/machine_factory.py
@@ -3,7 +3,6 @@
import sys
# Local package imports
-from .ibm_power9 import IBMpower9
from .linux_x86_64 import Linux_x86_64
from .machine_factory_exceptions import MachineTypeNotImplementedError
from .machine_factory_exceptions import MachineTypeUndefinedVariableError
diff --git a/harness/machine_types/base_scheduler.py b/harness/schedulers/base_scheduler.py
similarity index 100%
rename from harness/machine_types/base_scheduler.py
rename to harness/schedulers/base_scheduler.py
diff --git a/harness/machine_types/lsf.py b/harness/schedulers/lsf.py
similarity index 100%
rename from harness/machine_types/lsf.py
rename to harness/schedulers/lsf.py
diff --git a/harness/machine_types/pbs.py b/harness/schedulers/pbs.py
similarity index 100%
rename from harness/machine_types/pbs.py
rename to harness/schedulers/pbs.py
diff --git a/harness/machine_types/scheduler_factory.py b/harness/schedulers/scheduler_factory.py
similarity index 64%
rename from harness/machine_types/scheduler_factory.py
rename to harness/schedulers/scheduler_factory.py
index f0bca06c..29b904e6 100644
--- a/harness/machine_types/scheduler_factory.py
+++ b/harness/schedulers/scheduler_factory.py
@@ -6,14 +6,14 @@
class SchedulerFactory:
@staticmethod
- def create_scheduler(scheduler_type, logger):
+ def create_scheduler(scheduler_type, logger, use_jinja2=False):
tmp_scheduler = None
if scheduler_type == "LSF" or scheduler_type == "lsf":
- tmp_scheduler = LSF(logger=logger)
+ tmp_scheduler = LSF(logger=logger, use_jinja2=use_jinja2)
elif scheduler_type == "SLURM" or scheduler_type == "slurm":
- tmp_scheduler = SLURM(logger=logger)
+ tmp_scheduler = SLURM(logger=logger, use_jinja2=use_jinja2)
elif scheduler_type == "PBS" or scheduler_type == "pbs":
- tmp_scheduler = PBS(logger=logger)
+ tmp_scheduler = PBS(logger=logger, use_jinja2=use_jinja2)
else:
logger.doCriticalLogging("Scheduler not supported. Good bye!")
return tmp_scheduler
diff --git a/harness/machine_types/slurm.py b/harness/schedulers/slurm.py
similarity index 95%
rename from harness/machine_types/slurm.py
rename to harness/schedulers/slurm.py
index b701d1e1..5779a343 100644
--- a/harness/machine_types/slurm.py
+++ b/harness/schedulers/slurm.py
@@ -15,7 +15,7 @@ class SLURM(BaseScheduler):
""" SLURM class represents an SLURM scheduler. """
- def __init__(self, logger):
+ def __init__(self, logger, use_jinja2=False):
self.__name = 'SLURM'
self.__submitCmd = 'sbatch'
self.__statusCmd = 'squeue'
@@ -23,7 +23,7 @@ def __init__(self, logger):
self.__walltimeOpt = '-t'
self.__numTasksOpt = '-n'
self.__jobNameOpt = '-J'
- self.__templateFile = 'slurm.template.x'
+ self.__templateFile = 'slurm.template.x' if not use_jinja2 else 'slurm.template.j2'
self.__logger = logger
BaseScheduler.__init__(self, self.__name,
self.__submitCmd, self.__statusCmd, self.__deleteCmd,
diff --git a/harness/utilities/add_comment_to_databases.py b/harness/utilities/add_comment_to_databases.py
index a5bc1678..450d69d7 100755
--- a/harness/utilities/add_comment_to_databases.py
+++ b/harness/utilities/add_comment_to_databases.py
@@ -19,6 +19,11 @@
import csv
import socket
import re
+import sys
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
from libraries.rgt_database_loggers.rgt_database_logger_factory import create_rgt_db_logger
from libraries.rgt_database_loggers.db_backends.rgt_influxdb import InfluxDBLogger
diff --git a/harness/utilities/check_utility.py b/harness/utilities/check_utility.py
index 1657395f..24a1a5e8 100755
--- a/harness/utilities/check_utility.py
+++ b/harness/utilities/check_utility.py
@@ -3,6 +3,10 @@
import os
import sys
import getopt
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
from libraries.layout_of_apps_directory import apptest_layout
diff --git a/harness/utilities/list_tests.py b/harness/utilities/list_tests.py
index 003767c4..eb5db73c 100755
--- a/harness/utilities/list_tests.py
+++ b/harness/utilities/list_tests.py
@@ -1,6 +1,8 @@
#! /usr/bin/env python3
+
import glob
import os
+from pathlib import Path
"""
SYNOPSIS
@@ -26,7 +28,7 @@ def main():
#
# Ensure the path stored in "HARNESS_APPLICATION_PATH" exists.
#
- if not os.path.exists(HARNESS_APPLICATION_PATH):
+ if not Path(HARNESS_APPLICATION_PATH).exists():
tmp_string = "The path {0} does not exist.".format(HARNESS_APPLICATION_PATH)
print tmp_string
diff --git a/harness/utilities/report_to_databases.py b/harness/utilities/report_to_databases.py
index fdf67891..ea8f5c8e 100755
--- a/harness/utilities/report_to_databases.py
+++ b/harness/utilities/report_to_databases.py
@@ -13,6 +13,11 @@
import os
import argparse
import re
+import sys
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
from libraries.rgt_database_loggers.rgt_database_logger_factory import create_rgt_db_logger
from libraries.rgt_database_loggers.db_backends.rgt_influxdb import InfluxDBLogger
diff --git a/harness/utilities/rgt_archive_tests.py b/harness/utilities/rgt_archive_tests.py
index 17c1342b..58c61056 100755
--- a/harness/utilities/rgt_archive_tests.py
+++ b/harness/utilities/rgt_archive_tests.py
@@ -16,6 +16,10 @@
import shutil
import tarfile
import sys
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
# For directory names
from libraries.layout_of_apps_directory import apptest_layout
@@ -87,7 +91,7 @@ def check_time_format(s):
errs += 1
# Check if path to tests exists
- if not os.path.exists(args.path_to_tests):
+ if not Path(args.path_to_tests).exists():
logger.doCriticalLogging(f"Path to tests provided by --path-to-tests does not exist: {args.path_to_tests}")
errs += 1
################################################################################
@@ -138,7 +142,7 @@ def build_apptest_list():
exit(1)
# Make the output directory, if it doesn't exist
-if not os.path.exists(args.path_to_archive):
+if not Path(args.path_to_archive).exists():
logger.doInfoLogging(f"Creating output directory {args.path_to_archive}")
os.makedirs(args.path_to_archive)
@@ -246,7 +250,7 @@ def archive_test(apptest, test_id):
else:
logger.doWarningLogging(f"Found {apptest}/{test_id} in archive already at {test_archive_dir}. --force is set, so removing this directory.")
shutil.rmtree(test_archive_dir)
- elif os.path.exists(f'{test_archive_dir}.tar.gz'):
+ elif Path(f'{test_archive_dir}.tar.gz').exists():
if not args.force:
logger.doWarningLogging(f"Found a compressed {apptest}/{test_id} in archive already at {test_archive_dir}.tar.gz. Skipping.")
return False
@@ -266,7 +270,7 @@ def archive_test(apptest, test_id):
os.unlink(os.path.join(test_archive_dir, apptest_layout.test_build_dirname))
# if Status sym-link exists, then un-link, will copy later
- if os.path.exists(os.path.join(test_archive_dir, apptest_layout.test_status_dirname)):
+ if Path(test_archive_dir, apptest_layout.test_status_dirname).exists():
os.unlink(os.path.join(test_archive_dir, apptest_layout.test_status_dirname))
shutil.copytree(test_status, os.path.join(test_archive_dir, apptest_layout.test_status_dirname), symlinks=True)
diff --git a/harness/utilities/update_databases.py b/harness/utilities/update_databases.py
index 49ce23ed..8907a261 100755
--- a/harness/utilities/update_databases.py
+++ b/harness/utilities/update_databases.py
@@ -21,7 +21,12 @@
import argparse
import csv
import socket
+import sys
import re
+from pathlib import Path
+
+prefix = Path(__file__).resolve().parent.parent
+sys.path = [str(prefix)] + sys.path
from libraries.rgt_database_loggers.rgt_database_logger_factory import create_rgt_db_logger
@@ -507,7 +512,7 @@ def get_latest_time(t_new, t_ref):
status_file_path = os.path.join(entry['run_archive'], '..', '..', 'Status', entry['test_id'])
current_event_num = int(entry['event_filename'].split('_')[1])
cur_dir = os.getcwd()
- if not (os.path.exists(status_file_path) and os.path.exists(entry['run_archive'])):
+ if not (Path(status_file_path).exists() and Path(entry['run_archive']).exists()):
logger.doDebugLogging(f"Status file and Run_Archive paths for test {entry['test_id']} do not exist ({entry['run_archive']}). Skipping.")
continue
logger.doErrorLogging(f"Logging test that completed the Slurm job but did not log to the database, app={entry['app']}, test={entry['test']}, test_id={entry['test_id']}, jobid={entry['job_id']} to {db.url}.")
diff --git a/modulefiles/olcf_harness b/modulefiles/olcf_harness
index 171ff9a1..9e93c6b3 100755
--- a/modulefiles/olcf_harness
+++ b/modulefiles/olcf_harness
@@ -15,11 +15,3 @@ set harness $env(OLCF_HARNESS_DIR)/harness
setenv PATH_TO_RGT_PACKAGE $harness
prepend-path PATH $harness/bin
prepend-path PATH $harness/utilities
-prepend-path LD_LIBRARY_PATH $harness/libraries
-prepend-path LIBRARY_PATH $harness/libraries
-
-prepend-path PYTHONPATH $harness/utilities
-prepend-path PYTHONPATH $harness/bin
-prepend-path PYTHONPATH $harness/libraries
-prepend-path PYTHONPATH $harness
-