From bd0baf3e2545ab56fd515add907cceb388667609 Mon Sep 17 00:00:00 2001 From: Pietro Pasotti Date: Thu, 4 Jan 2024 17:44:03 +0100 Subject: [PATCH] support unified charmcraft model --- scenario/state.py | 43 ++++++++++++++++++++++++------- tests/test_charm_spec_autoload.py | 42 +++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/scenario/state.py b/scenario/state.py index e5fb1d1c..edbed688 100644 --- a/scenario/state.py +++ b/scenario/state.py @@ -947,6 +947,8 @@ class _CharmSpec(_DCBase): """Charm spec.""" charm_type: Type["CharmType"] + + # TODO: consider unifying the data model since nowadays it's all in one file meta: Optional[Dict[str, Any]] actions: Optional[Dict[str, Any]] = None config: Optional[Dict[str, Any]] = None @@ -956,16 +958,10 @@ class _CharmSpec(_DCBase): is_autoloaded: bool = False @staticmethod - def autoload(charm_type: Type["CharmType"]): - charm_source_path = Path(inspect.getfile(charm_type)) - charm_root = charm_source_path.parent.parent - + def _load_metadata_legacy(charm_root: Path): + # back in the days, we used to have separate metadata.yaml, config.yaml and actions.yaml + # files for charm metadata. metadata_path = charm_root / "metadata.yaml" - if not metadata_path.exists(): - raise MetadataNotFoundError( - f"invalid charm root {charm_root!r}; " - f"expected to contain at least a `metadata.yaml` file.", - ) meta = yaml.safe_load(metadata_path.open()) actions = config = None @@ -978,6 +974,35 @@ def autoload(charm_type: Type["CharmType"]): if actions_path.exists(): actions = yaml.safe_load(actions_path.open()) + return meta, config, actions + + @staticmethod + def _load_metadata(charm_root: Path): + metadata_path = charm_root / "charmcraft.yaml" + meta = yaml.safe_load(metadata_path.open()) if metadata_path.exists() else {} + config = meta.get("config", None) + actions = meta.get("actions", None) + return meta, config, actions + + @staticmethod + def autoload(charm_type: Type["CharmType"]): + charm_source_path = Path(inspect.getfile(charm_type)) + charm_root = charm_source_path.parent.parent + + metadata_path = charm_root / "metadata.yaml" + + if metadata_path.exists(): + meta, config, actions = _CharmSpec._load_metadata_legacy(charm_root) + else: + meta, config, actions = _CharmSpec._load_metadata(charm_root) + + if not meta: + raise MetadataNotFoundError( + f"invalid charm root {charm_root!r}; " + f"expected to contain at least a `charmcraft.yaml` file " + f"(or a `metadata.yaml` file if it's an old charm).", + ) + return _CharmSpec( charm_type=charm_type, meta=meta, diff --git a/tests/test_charm_spec_autoload.py b/tests/test_charm_spec_autoload.py index 65ff83d2..48ea9097 100644 --- a/tests/test_charm_spec_autoload.py +++ b/tests/test_charm_spec_autoload.py @@ -38,51 +38,69 @@ def create_tempcharm( actions=None, config=None, name: str = "MyCharm", + legacy: bool = True, ): src = root / "src" src.mkdir(parents=True) charmpy = src / "charm.py" charmpy.write_text(charm) - if meta is not None: - (root / "metadata.yaml").write_text(yaml.safe_dump(meta)) + if legacy: + if meta is not None: + (root / "metadata.yaml").write_text(yaml.safe_dump(meta)) - if actions is not None: - (root / "actions.yaml").write_text(yaml.safe_dump(actions)) + if actions is not None: + (root / "actions.yaml").write_text(yaml.safe_dump(actions)) - if config is not None: - (root / "config.yaml").write_text(yaml.safe_dump(config)) + if config is not None: + (root / "config.yaml").write_text(yaml.safe_dump(config)) + else: + unified_meta = meta or {} + if actions: + unified_meta["actions"] = actions + if config: + unified_meta["config"] = config + if unified_meta: + (root / "charmcraft.yaml").write_text(yaml.safe_dump(unified_meta)) with import_name(name, charmpy) as charm: yield charm -def test_meta_autoload(tmp_path): - with create_tempcharm(tmp_path, meta={"name": "foo"}) as charm: +@pytest.mark.parametrize("legacy", (True, False)) +def test_meta_autoload(tmp_path, legacy): + with create_tempcharm(tmp_path, legacy=legacy, meta={"name": "foo"}) as charm: ctx = Context(charm) ctx.run("start", State()) -def test_no_meta_raises(tmp_path): +@pytest.mark.parametrize("legacy", (True, False)) +def test_no_meta_raises(tmp_path, legacy): with create_tempcharm( tmp_path, + legacy=legacy, ) as charm: # metadata not found: with pytest.raises(ContextSetupError): Context(charm) -def test_relations_ok(tmp_path): +@pytest.mark.parametrize("legacy", (True, False)) +def test_relations_ok(tmp_path, legacy): with create_tempcharm( - tmp_path, meta={"name": "josh", "requires": {"cuddles": {"interface": "arms"}}} + tmp_path, + legacy=legacy, + meta={"name": "josh", "requires": {"cuddles": {"interface": "arms"}}}, ) as charm: # this would fail if there were no 'cuddles' relation defined in meta Context(charm).run("start", State(relations=[Relation("cuddles")])) -def test_config_defaults(tmp_path): +@pytest.mark.parametrize("legacy", (True, False)) +def test_config_defaults(tmp_path, legacy): with create_tempcharm( tmp_path, + legacy=legacy, meta={"name": "josh"}, config={"options": {"foo": {"type": "bool", "default": True}}}, ) as charm: