# Configuration Helpers in the Artefacts Toolkit

Site [llms.txt](/llms.txt)

---

The Artefacts Toolkit Configuration Helpers are functions that allow you to interact with configurations you have set in your `artefacts.yaml` file.

Import with:
```python
from artefacts_toolkit.config import get_artefacts_param
```

## Functions
* [`get_artefacts_param`](#get_artefacts_param)
* [`get_artefacts_params`](#get_artefacts_params)
* [`merge_ros_params_files`](#merge_ros_params_files)

## Function Reference

### `get_artefacts_param`

Returns a parameter set in the `artefacts.yaml` file. If `param_type` is set to `launch`, it will be returned as type `string` so that it can be used as a ROS launch argument.

This function is particularly useful for parametric testing. When you define a list of parameter values in your `artefacts.yaml` file (e.g., `launch/world: ["empty.sdf", "bookstore.sdf", "restaurant.sdf"]`), the `artefacts run` command will execute your test multiple times - once for each value in the list. During each test execution, `get_artefacts_param("launch", "world")` will automatically return the current parameter value for that specific test run.

This becomes especially powerful for grid-based testing! With three parameters that each have three possible values, Artefacts will automatically execute your test 27 times (3 × 3 × 3) while your launch file code remains unchanged.

```python
get_artefacts_param(
    param_type,
    param_name,
    default=None,
    is_ros=True
)
```

#### Parameters

| **Parameter** | **Type** | **Description**                                                            | **Default** |
| ------------- | -------- | -------------------------------------------------------------------------- | ----------- |
| `param_type`  | `str`    | The namespace/category of the parameter (e.g., "launch" in "launch/world") | Required    |
| `param_name`  | `str`    | The specific parameter name (e.g., "world" in "launch/world")              | Required    |
| `default`     | `any`    | Value to return if the parameter isn't found in artefacts.yaml.            | `None`      |
| `is_ros`      | `bool`   | Whether the parameter should be converted to a ROS parameter format        | `True`      |

#### Returns

The function returns the parameter value with the following behavior:

- If `param_type` is `"launch"` and `is_ros` is `True`: Returns the value as a `str`, regardless of its original type in the `artefacts.yaml` file. This is so that it can be used as a ros launch argument.
- if `default` is set to anything other than `None`, then the value will be returned if artefacts is unable to find the requested parameter. This can be useful when (for example) using `launch_test` instead of `artefacts run` but you do not wish to make any code changes. It will also prevent `KeyError` exceptions.
- For all other cases: Returns the value with its original type from the YAML file (e.g., `list`, `dict`, `int`, `float`, `str`, etc.)

> [!NOTE]
> * Currently only supports ROS Params / ROS launch arguments.
> * Parametric tests will execute sequentially when running artefacts locally. When running on the platform, they will run in parallel (the maximum number of concurrent tests is determined by your subscription plan).

#### Example

Given an `artefacts.yaml` file with the following configuration set:

```yaml
scenarios:
  defaults:
    output_dirs: ["output"]
    metrics:
        - /odometry_error
    params:
      launch/world: ["bookstore.sdf", "empty.sdf"]
```

Getting the parameter in our test launch file like so:

```python
def generate_test_description():
    try:
        world = get_artefacts_param("launch", "world")
    except FileNotFoundError:
        world = "empty.world"

    run_headless = LaunchConfiguration("run_headless")
    launch_navigation_stack = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            [
                os.path.join(
                    get_package_share_directory("sam_bot_nav2_gz"),
                    "launch",
                    "complete_navigation.launch.py"
                ),
            ]
        ),
        launch_arguments=[("run_headless", run_headless), ("world_file", world)],
    )

...# Rest of launch test file   
```

This will run the same test twice - once in an empty world and once in a bookstore world - without any changes to your launch file code between runs.

---


### `get_artefacts_params`

Loads all parameters set in `artefacts.yaml` for a given run, providing them as `dict`. Can be used with regular dict methods such as `.get()`

```python
get_artefacts_params()
```

#### Parameters

None

#### Returns

The function returns all parameters for a given run as type `dict`.

> [!NOTE]
> What is returned will differ depending on whether you are using `launch_test_file` not in your `artefacts.yaml` file to point to your test file. With `launch_test_file` the parameters will be "rosified", otherwise (e.g with `pytest_file`, `run`) they will not. As an example: 
>
> ```yaml
> scenarios:
>   defaults:
>     output_dirs: ["output"]
>     metrics:
>         - /odometry_error
>     params:
>       my_test_node/parameter_1: [0.1, 0.2]
>       my_test_node/parameter_2: ["a", "b"]
> ```
>
> will return the following when using `launch_test_file`:
>
> ```python
> # Run 1
> {
>     'my_test_node': {
>         'ros__parameters': {
>             'parameter_1': 0.1,
>             'parameter_2': 'a'
>         }
>     }
> }
>
> # Run 2
> {
>     'my_test_node': {
>         'ros__parameters': {
>             'parameter_1': 0.2,
>             'parameter_2': 'b'
>         }
>     }
> }
> ```
>
> and the following when using `pytest_file`:
>
> ```python
> # Run 1
> {
>     'my_test_node/parameter_1': 0.1,
>     'my_test_node/parameter_2': 'a'
>
> }
>
> # Run 2
> {
>     'my_test_node/parameter_1': 0.2,
>     'my_test_node/parameter_2': 'b'
>
> }
> ```

#### Example

Given an `artefacts.yaml` file with the following configuration set:

```yaml
scenarios:
    defaults:
        params:
            headless: "True"
```

We can set a flag on our launch_description for a headless simulation as follows:

```python
@launch_pytest.fixture(scope="module")
def launch_description(rosbag_recording):
    pkg_path = Path(get_package_share_directory("my_package"))
    try:
        headless = get_artefacts_params().get("headless", "False")
    except RuntimeError:
        # If artefacts params are not available, default to False
        headless = "False"

    start_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            str(pkg_path / "launch" / "mytestfile.launch.py")
        ),
        launch_arguments={
            "headless": headless,
        }.items(),
    )
```

---

### merge_ros_params_files

Merges two ROS2 yaml parameter files into one, overriding the values in the first one (`source`) with the values in `override`

```python
merge_ros_params_files(
    source,
    override,
    destination,
    rosify=False
)
```

#### Parameters

| **Parameter** | **Type** | **Description**                                               | **Default** |
| ------------- | -------- | ------------------------------------------------------------- | ----------- |
| `source`      | `str`    | Path to original parameter file _to be_ overridden            | Required    |
| `override`    | `str`    | Path to parameter file containing parameters _to_ override    | Required    |
| `destination` | `str`    | Path to where new parameter file should be saved              | Required    |
| `rosify`      | `bool`   | Converts params to ros2 param file nested format if `True`    | `False`     |


#### Returns

`None`: The newly made merged parameter file will be available at the path specified in `destination`

### Example

Given the following `source` and `override` yaml:

```yaml
# source
outer:
    inner1: 1
    inner2: 
        leaf: keep
flat: src

# override
outer:
    inner2:
        leaf: replaced
        new_leaf: 321
    inner3: added
flat: override
```

We will get the following new yaml file saved to `destination` as follows:

```yaml
# rosify=False (Default)
outer:
    inner1: 1
    inner2:
        leaf: replaced
        new_leaf: 321
    inner3: added
flat: override
```

With `rosify=True`, parameters will be nested under `ros__parameters` for the appropriate node:

```yaml
#source
controller_server:
    ros__parameters:
        enable_stamped_cmd_vel: true
        controller_frequency: 20.0
        min_x_velocity_threshold: 0.001

#override
controller_server/controller_frequency: 30
controller_server/min_x_velocity_threshold: 0.005
controller_server/min_y_velocity_threshold: 0.5   
    
#destination
controller_server:
    ros__parameters:
        enable_stamped_cmd_vel: true
        controller_frequency: 30
        min_x_velocity_threshold: 0.005
        min_y_velocity_threshold: 0.5
```
