Repository: canonical/charmcraft

Issue:
Move additional YAML file loads into _extra_yaml_transform
### What needs to get done

Instead of using the CharmcraftProject model's `from_yaml_file` method to do everything, preprocess the following files in functions called from _extra_yaml_transform:

- [ ] bundle.yaml (this can be added to the `_bundle` extension)
- [ ] metadata.yaml
- [ ] config.yaml
- [ ] actions.yaml

### Why it needs to get done

That's a very long method that really should be broken up and moved to craft-application 2.0 standards

Discussion:
syu-w: I wonder if this affect reactive charm

Patch:
diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py
index cf4b60a7..cd59750f 100644
--- a/charmcraft/application/main.py
+++ b/charmcraft/application/main.py
@@ -69,6 +69,8 @@ class Charmcraft(Application):
 
         preprocess.add_default_parts(yaml_data)
         preprocess.add_bundle_snippet(self.project_dir, yaml_data)
+        preprocess.add_config(self.project_dir, yaml_data)
+        preprocess.add_actions(self.project_dir, yaml_data)
 
         yaml_data = extensions.apply_extensions(self.project_dir, yaml_data)
 
diff --git a/charmcraft/models/project.py b/charmcraft/models/project.py
index 4ee97e5b..51ae653f 100644
--- a/charmcraft/models/project.py
+++ b/charmcraft/models/project.py
@@ -34,14 +34,10 @@ from typing_extensions import Self, TypedDict
 
 from charmcraft import const, preprocess, utils
 from charmcraft.const import (
-    JUJU_ACTIONS_FILENAME,
-    JUJU_CONFIG_FILENAME,
     BaseStr,
     BuildBaseStr,
     CharmArch,
 )
-from charmcraft.metafiles.actions import parse_actions_yaml
-from charmcraft.metafiles.config import parse_config_yaml
 from charmcraft.models import charmcraft
 from charmcraft.models.charmcraft import (
     AnalysisConfig,
@@ -440,24 +436,8 @@ class CharmcraftProject(models.Project, metaclass=abc.ABCMeta):
         preprocess.add_default_parts(data)
         preprocess.add_bundle_snippet(project_dir, data)
         preprocess.add_metadata(project_dir, data)
-
-        config_file = project_dir / JUJU_CONFIG_FILENAME
-        if config_file.is_file():
-            if "config" in data:
-                raise errors.CraftValidationError(
-                    f"Cannot specify 'config' section in 'charmcraft.yaml' when {JUJU_CONFIG_FILENAME!r} exists",
-                    resolution=f"Move all data from {JUJU_CONFIG_FILENAME!r} to the 'config' section in 'charmcraft.yaml'",
-                )
-            data["config"] = parse_config_yaml(project_dir, allow_broken=True)
-
-        actions_file = project_dir / JUJU_ACTIONS_FILENAME
-        if actions_file.is_file():
-            if "actions" in data:
-                raise errors.CraftValidationError(
-                    f"Cannot specify 'actions' section in 'charmcraft.yaml' when {JUJU_ACTIONS_FILENAME!r} exists",
-                    resolution=f"Move all data from {JUJU_ACTIONS_FILENAME!r} to the 'actions' section in 'charmcraft.yaml'",
-                )
-            data["actions"] = parse_actions_yaml(project_dir).actions
+        preprocess.add_config(project_dir, data)
+        preprocess.add_actions(project_dir, data)
 
         try:
             project = cls.unmarshal(data)
diff --git a/charmcraft/preprocess.py b/charmcraft/preprocess.py
index 084fb4d3..08295335 100644
--- a/charmcraft/preprocess.py
+++ b/charmcraft/preprocess.py
@@ -21,9 +21,9 @@ to do pre-processing on a charmcraft.yaml file before applying extensions.
 import pathlib
 from typing import Any
 
-from craft_application import util
+from craft_application import errors, util
 
-from charmcraft import const, errors, utils
+from charmcraft import const, utils
 
 
 def add_default_parts(yaml_data: dict[str, Any]) -> None:
@@ -54,7 +54,7 @@ def add_metadata(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
     with metadata_path.open() as file:
         metadata_yaml = util.safe_yaml_load(file)
     if not isinstance(metadata_yaml, dict):
-        raise errors.CraftError(
+        raise errors.CraftValidationError(
             "Invalid file: 'metadata.yaml'",
             resolution="Ensure metadata.yaml meets the juju metadata.yaml specification.",
             docs_url="https://juju.is/docs/sdk/metadata-yaml",
@@ -66,7 +66,7 @@ def add_metadata(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
             duplicate_fields.append(field)
         yaml_data.setdefault(field, metadata_yaml.get(field))
     if duplicate_fields:
-        raise errors.CraftError(
+        raise errors.CraftValidationError(
             "Fields in charmcraft.yaml cannot be duplicated in metadata.yaml",
             details=f"Duplicate fields: {utils.humanize_list(duplicate_fields, 'and')}",
             resolution="Remove the duplicate fields from metadata.yaml.",
@@ -74,7 +74,7 @@ def add_metadata(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
         )
 
 
-def add_bundle_snippet(project_dir: pathlib.Path, yaml_data: dict[str, Any]):
+def add_bundle_snippet(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
     """Add metadata from bundle.yaml to a bundle.
 
     :param yaml_data: The raw YAML dictionary of the project.
@@ -86,7 +86,7 @@ def add_bundle_snippet(project_dir: pathlib.Path, yaml_data: dict[str, Any]):
 
     bundle_file = project_dir / const.BUNDLE_FILENAME
     if not bundle_file.is_file():
-        raise errors.CraftError(
+        raise errors.CraftValidationError(
             f"Missing 'bundle.yaml' file: {str(bundle_file)!r}",
             resolution="Create a 'bundle.yaml' file in the same directory as 'charmcraft.yaml'.",
             docs_url="https://juju.is/docs/sdk/create-a-charm-bundle",
@@ -97,7 +97,7 @@ def add_bundle_snippet(project_dir: pathlib.Path, yaml_data: dict[str, Any]):
     with bundle_file.open() as bf:
         bundle = util.safe_yaml_load(bf)
     if not isinstance(bundle, dict):
-        raise errors.CraftError(
+        raise errors.CraftValidationError(
             "Incorrectly formatted 'bundle.yaml' file",
             resolution="Ensure 'bundle.yaml' matches the Juju 'bundle.yaml' format.",
             docs_url="https://juju.is/docs/sdk/charm-bundles",
@@ -105,3 +105,50 @@ def add_bundle_snippet(project_dir: pathlib.Path, yaml_data: dict[str, Any]):
             retcode=65,  # EX_DATAERR from sysexits.h
         )
     yaml_data["bundle"] = bundle
+
+
+def add_config(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
+    """Add configuration options from config.yaml to existing YAML data.
+
+    :param project_dir: The Path to the directory containing charmcraft.yaml
+    :param yaml_data: The raw YAML dictionary of the project.
+    :returns: The same dictionary passed in, with necessary mutations.
+    """
+    config_file = project_dir / const.JUJU_CONFIG_FILENAME
+    if not config_file.exists():
+        return
+
+    if "config" in yaml_data:
+        raise errors.CraftValidationError(
+            f"Cannot specify 'config' section in 'charmcraft.yaml' when {const.JUJU_CONFIG_FILENAME!r} exists",
+            resolution=f"Move all data from {const.JUJU_CONFIG_FILENAME!r} to the 'config' section in 'charmcraft.yaml'",
+            docs_url="https://juju.is/docs/sdk/charmcraft-yaml",
+            retcode=65,  # Data error, per sysexits.h
+        )
+
+    with config_file.open() as f:
+        yaml_data["config"] = util.safe_yaml_load(f)
+
+
+def add_actions(project_dir: pathlib.Path, yaml_data: dict[str, Any]) -> None:
+    """Add actions from actions.yaml to existing YAML data.
+
+    :param project_dir: The Path to the directory containing charmcraft.yaml
+    :param yaml_data: The raw YAML dictionary of the project.
+    :returns: The same dictionary passed in, with necessary mutations.
+    """
+    actions_file = project_dir / const.JUJU_ACTIONS_FILENAME
+    if not actions_file.exists():
+        return
+
+    if "actions" in yaml_data:
+        raise errors.CraftValidationError(
+            f"Cannot specify 'actions' section in 'charmcraft.yaml' when {const.JUJU_ACTIONS_FILENAME!r} exists",
+            resolution=f"Move all data from {const.JUJU_ACTIONS_FILENAME!r} to the 'actions' section in 'charmcraft.yaml'",
+            docs_url="https://juju.is/docs/sdk/charmcraft-yaml",
+            retcode=65,  # Data error, per sysexits.h
+        )
+    with actions_file.open() as f:
+        actions = util.safe_yaml_load(f)
+    if actions and isinstance(actions, dict):
+        yaml_data["actions"] = actions


Test patch:
diff --git a/tests/integration/sample-charms/actions-included/charmcraft.yaml b/tests/integration/sample-charms/actions-included/charmcraft.yaml
new file mode 100644
index 00000000..23239d7b
--- /dev/null
+++ b/tests/integration/sample-charms/actions-included/charmcraft.yaml
@@ -0,0 +1,38 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+type: charm
+base: ubuntu@22.04
+platforms:
+  amd64:
+
+parts:
+  charm:
+    plugin: charm
+
+actions:
+  pause:
+    description: Pause the database.
+  resume:
+    description: Resume a paused database.
+  snapshot:
+    description: Take a snapshot of the database.
+    params:
+      filename:
+        type: string
+        description: The name of the snapshot file.
+      compression:
+        type: object
+        description: The type of compression to use.
+        properties:
+          kind:
+            type: string
+            enum: [gzip, bzip2, xz]
+          quality:
+            description: Compression quality
+            type: integer
+            minimum: 0
+            maximum: 9
+    required: [filename]
+    additionalProperties: false
diff --git a/tests/integration/sample-charms/actions-included/expected.yaml b/tests/integration/sample-charms/actions-included/expected.yaml
new file mode 100644
index 00000000..074bf477
--- /dev/null
+++ b/tests/integration/sample-charms/actions-included/expected.yaml
@@ -0,0 +1,46 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+base: ubuntu@22.04
+platforms:
+  amd64: null
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
+actions:
+  pause:
+    description: Pause the database.
+  resume:
+    description: Resume a paused database.
+  snapshot:
+    description: Take a snapshot of the database.
+    params:
+      filename:
+        type: string
+        description: The name of the snapshot file.
+      compression:
+        type: object
+        description: The type of compression to use.
+        properties:
+          kind:
+            type: string
+            enum:
+            - gzip
+            - bzip2
+            - xz
+          quality:
+            description: Compression quality
+            type: integer
+            minimum: 0
+            maximum: 9
+    required:
+    - filename
+    additionalProperties: false
diff --git a/tests/integration/sample-charms/actions-separate/actions.yaml b/tests/integration/sample-charms/actions-separate/actions.yaml
new file mode 100644
index 00000000..b155d4b8
--- /dev/null
+++ b/tests/integration/sample-charms/actions-separate/actions.yaml
@@ -0,0 +1,24 @@
+pause:
+  description: Pause the database.
+resume:
+  description: Resume a paused database.
+snapshot:
+  description: Take a snapshot of the database.
+  params:
+    filename:
+      type: string
+      description: The name of the snapshot file.
+    compression:
+      type: object
+      description: The type of compression to use.
+      properties:
+        kind:
+          type: string
+          enum: [gzip, bzip2, xz]
+        quality:
+          description: Compression quality
+          type: integer
+          minimum: 0
+          maximum: 9
+  required: [filename]
+  additionalProperties: false
diff --git a/tests/integration/sample-charms/actions-separate/charmcraft.yaml b/tests/integration/sample-charms/actions-separate/charmcraft.yaml
new file mode 100644
index 00000000..d681da9a
--- /dev/null
+++ b/tests/integration/sample-charms/actions-separate/charmcraft.yaml
@@ -0,0 +1,12 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+type: charm
+base: ubuntu@22.04
+platforms:
+  amd64:
+
+parts:
+  charm:
+    plugin: charm
diff --git a/tests/integration/sample-charms/actions-separate/expected.yaml b/tests/integration/sample-charms/actions-separate/expected.yaml
new file mode 100644
index 00000000..074bf477
--- /dev/null
+++ b/tests/integration/sample-charms/actions-separate/expected.yaml
@@ -0,0 +1,46 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+base: ubuntu@22.04
+platforms:
+  amd64: null
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
+actions:
+  pause:
+    description: Pause the database.
+  resume:
+    description: Resume a paused database.
+  snapshot:
+    description: Take a snapshot of the database.
+    params:
+      filename:
+        type: string
+        description: The name of the snapshot file.
+      compression:
+        type: object
+        description: The type of compression to use.
+        properties:
+          kind:
+            type: string
+            enum:
+            - gzip
+            - bzip2
+            - xz
+          quality:
+            description: Compression quality
+            type: integer
+            minimum: 0
+            maximum: 9
+    required:
+    - filename
+    additionalProperties: false
diff --git a/tests/integration/sample-charms/basic-bases/charmcraft.yaml b/tests/integration/sample-charms/basic-bases/charmcraft.yaml
new file mode 100644
index 00000000..3e078462
--- /dev/null
+++ b/tests/integration/sample-charms/basic-bases/charmcraft.yaml
@@ -0,0 +1,12 @@
+name: example-charm
+summary: An example charm with bases
+description: |
+  A description for an example charm with bases.
+type: charm
+bases:
+  - name: ubuntu
+    channel: "22.04"
+
+parts:
+  charm:
+    plugin: charm
diff --git a/tests/integration/sample-charms/basic-bases/expected.yaml b/tests/integration/sample-charms/basic-bases/expected.yaml
new file mode 100644
index 00000000..a7f02d3d
--- /dev/null
+++ b/tests/integration/sample-charms/basic-bases/expected.yaml
@@ -0,0 +1,21 @@
+name: example-charm
+summary: An example charm with bases
+description: |
+  A description for an example charm with bases.
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
+bases:
+- build-on:
+  - name: ubuntu
+    channel: '22.04'
+  run-on:
+  - name: ubuntu
+    channel: '22.04'
diff --git a/tests/integration/sample-charms/basic-platforms/charmcraft.yaml b/tests/integration/sample-charms/basic-platforms/charmcraft.yaml
new file mode 100644
index 00000000..d681da9a
--- /dev/null
+++ b/tests/integration/sample-charms/basic-platforms/charmcraft.yaml
@@ -0,0 +1,12 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+type: charm
+base: ubuntu@22.04
+platforms:
+  amd64:
+
+parts:
+  charm:
+    plugin: charm
diff --git a/tests/integration/sample-charms/basic-platforms/expected.yaml b/tests/integration/sample-charms/basic-platforms/expected.yaml
new file mode 100644
index 00000000..95213900
--- /dev/null
+++ b/tests/integration/sample-charms/basic-platforms/expected.yaml
@@ -0,0 +1,17 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+base: ubuntu@22.04
+platforms:
+  amd64: null
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
diff --git a/tests/integration/sample-charms/config-included/charmcraft.yaml b/tests/integration/sample-charms/config-included/charmcraft.yaml
new file mode 100644
index 00000000..44a0837a
--- /dev/null
+++ b/tests/integration/sample-charms/config-included/charmcraft.yaml
@@ -0,0 +1,33 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+type: charm
+base: ubuntu@22.04
+platforms:
+  amd64:
+
+parts:
+  charm:
+    plugin: charm
+
+config:
+  options:
+    admins:
+      description: 'Comma-separated list of admin users to create: user:pass[,user:pass]+'
+      type: string
+    debug:
+      default: false
+      description: turn on debugging features of mediawiki
+      type: boolean
+    logo:
+      description: URL to fetch logo from
+      type: string
+    name:
+      default: Wiki
+      description: The name, or Title of the Wiki
+      type: string
+    skin:
+      default: vector
+      description: skin for the Wiki
+      type: string
diff --git a/tests/integration/sample-charms/config-included/expected.yaml b/tests/integration/sample-charms/config-included/expected.yaml
new file mode 100644
index 00000000..ac875e8e
--- /dev/null
+++ b/tests/integration/sample-charms/config-included/expected.yaml
@@ -0,0 +1,37 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+base: ubuntu@22.04
+platforms:
+  amd64: null
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
+config:
+  options:
+    admins:
+      description: 'Comma-separated list of admin users to create: user:pass[,user:pass]+'
+      type: string
+    debug:
+      default: false
+      description: turn on debugging features of mediawiki
+      type: boolean
+    logo:
+      description: URL to fetch logo from
+      type: string
+    name:
+      default: Wiki
+      description: The name, or Title of the Wiki
+      type: string
+    skin:
+      default: vector
+      description: skin for the Wiki
+      type: string
diff --git a/tests/integration/sample-charms/config-separate/charmcraft.yaml b/tests/integration/sample-charms/config-separate/charmcraft.yaml
new file mode 100644
index 00000000..d681da9a
--- /dev/null
+++ b/tests/integration/sample-charms/config-separate/charmcraft.yaml
@@ -0,0 +1,12 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+type: charm
+base: ubuntu@22.04
+platforms:
+  amd64:
+
+parts:
+  charm:
+    plugin: charm
diff --git a/tests/integration/sample-charms/config-separate/config.yaml b/tests/integration/sample-charms/config-separate/config.yaml
new file mode 100644
index 00000000..4700abb9
--- /dev/null
+++ b/tests/integration/sample-charms/config-separate/config.yaml
@@ -0,0 +1,19 @@
+options:
+  admins:
+    description: 'Comma-separated list of admin users to create: user:pass[,user:pass]+'
+    type: string
+  debug:
+    default: false
+    description: turn on debugging features of mediawiki
+    type: boolean
+  logo:
+    description: URL to fetch logo from
+    type: string
+  name:
+    default: Wiki
+    description: The name, or Title of the Wiki
+    type: string
+  skin:
+    default: vector
+    description: skin for the Wiki
+    type: string
diff --git a/tests/integration/sample-charms/config-separate/expected.yaml b/tests/integration/sample-charms/config-separate/expected.yaml
new file mode 100644
index 00000000..ac875e8e
--- /dev/null
+++ b/tests/integration/sample-charms/config-separate/expected.yaml
@@ -0,0 +1,37 @@
+name: example-charm
+summary: An example charm with platforms
+description: |
+  A description for an example charm with platforms.
+base: ubuntu@22.04
+platforms:
+  amd64: null
+parts:
+  charm:
+    source: .
+    charm-entrypoint: src/charm.py
+    charm-binary-python-packages: []
+    charm-python-packages: []
+    charm-requirements: []
+    charm-strict-dependencies: false
+    plugin: charm
+type: charm
+config:
+  options:
+    admins:
+      description: 'Comma-separated list of admin users to create: user:pass[,user:pass]+'
+      type: string
+    debug:
+      default: false
+      description: turn on debugging features of mediawiki
+      type: boolean
+    logo:
+      description: URL to fetch logo from
+      type: string
+    name:
+      default: Wiki
+      description: The name, or Title of the Wiki
+      type: string
+    skin:
+      default: vector
+      description: skin for the Wiki
+      type: string
diff --git a/tests/integration/test_application.py b/tests/integration/test_application.py
new file mode 100644
index 00000000..d8fe7aea
--- /dev/null
+++ b/tests/integration/test_application.py
@@ -0,0 +1,37 @@
+# Copyright 2024 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# For further info, check https://github.com/canonical/charmcraft
+"""Integration tests for the Charmcraft class."""
+import pathlib
+
+import pytest
+from craft_application import util
+
+from charmcraft import models
+
+
+@pytest.mark.parametrize(
+    "charm_dir", sorted((pathlib.Path(__file__).parent / "sample-charms").iterdir())
+)
+def test_load_charm(app, charm_dir):
+    app.project_dir = charm_dir
+
+    project = app.get_project()
+    with (charm_dir / "expected.yaml").open() as f:
+        expected_data = util.safe_yaml_load(f)
+    expected_project = models.CharmcraftProject.unmarshal(expected_data)
+
+    assert project == expected_project
+    assert util.dump_yaml(project.marshal()) == (charm_dir / "expected.yaml").read_text()
diff --git a/tests/unit/test_preprocess.py b/tests/unit/test_preprocess.py
index 8bc4618a..c7fb429f 100644
--- a/tests/unit/test_preprocess.py
+++ b/tests/unit/test_preprocess.py
@@ -142,3 +142,40 @@ def test_add_bundle_snippet_invalid_file(fs, contents):
         preprocess.add_bundle_snippet(pathlib.Path("project"), {"type": "bundle"})
 
     assert exc_info.value.retcode == 65
+
+
+@pytest.mark.parametrize(
+    ("yaml_data", "config_yaml", "expected"),
+    [
+        ({}, "{}", {"config": {}}),
+        ({}, "options:\n boop:\n  type: int", {"config": {"options": {"boop": {"type": "int"}}}}),
+    ],
+)
+def test_add_config_success(fs, yaml_data, config_yaml, expected):
+    project_dir = pathlib.Path("project")
+    fs.create_file(project_dir / const.JUJU_CONFIG_FILENAME, contents=config_yaml)
+
+    preprocess.add_config(project_dir, yaml_data)
+
+    assert yaml_data == expected
+
+
+@pytest.mark.parametrize(
+    ("yaml_data", "actions_yaml", "expected"),
+    [
+        ({}, "", {}),
+        ({}, "{}", {}),
+        (
+            {},
+            "boop:\n description: Boop in the snoot",
+            {"actions": {"boop": {"description": "Boop in the snoot"}}},
+        ),
+    ],
+)
+def test_add_actions_success(fs, yaml_data, actions_yaml, expected):
+    project_dir = pathlib.Path("project")
+    fs.create_file(project_dir / const.JUJU_ACTIONS_FILENAME, contents=actions_yaml)
+
+    preprocess.add_actions(project_dir, yaml_data)
+
+    assert yaml_data == expected