diff --git a/ops/testing.py b/ops/testing.py index fd69a1968..9710e6098 100755 --- a/ops/testing.py +++ b/ops/testing.py @@ -389,57 +389,102 @@ def cleanup(self) -> None: """ self._backend._cleanup() - def _create_meta(self, charm_metadata: Optional[YAMLStringOrFile], - action_metadata: Optional[YAMLStringOrFile]) -> CharmMeta: + def _create_meta(self, charm_metadata_yaml: Optional[YAMLStringOrFile], + action_metadata_yaml: Optional[YAMLStringOrFile]) -> CharmMeta: """Create a CharmMeta object. Handle the cases where a user doesn't supply explicit metadata snippets. + This will try to load metadata from ``/charmcraft.yaml`` first, then + ``/metadata.yaml`` if charmcraft.yaml does not include metadata, + and ``/actions.yaml`` if charmcraft.yaml does not include actions. """ filename = inspect.getfile(self._charm_cls) charm_dir = pathlib.Path(filename).parents[1] + charm_metadata: Optional[Dict[str, Any]] = None + charmcraft_metadata: Optional[Dict[str, Any]] = None + # Check charmcraft.yaml and load it if it exists + charmcraft_meta = charm_dir / "charmcraft.yaml" + if charmcraft_meta.is_file(): + self._charm_dir = charm_dir + charmcraft_metadata = yaml.safe_load(charmcraft_meta.read_text()) + + # Load metadata from parameters if provided + if charm_metadata_yaml is not None: + if isinstance(charm_metadata_yaml, str): + charm_metadata_yaml = dedent(charm_metadata_yaml) + charm_metadata = yaml.safe_load(charm_metadata_yaml) + else: + # Check charmcraft.yaml for metadata if no metadata is provided + if charmcraft_metadata is not None: + meta_keys = ["name", "summary", "description"] + if any(key in charmcraft_metadata for key in meta_keys): + # Unrelated keys in the charmcraft.yaml file will be ignored. + charm_metadata = charmcraft_metadata + + # Still no metadata, check metadata.yaml + if charm_metadata is None: + metadata_path = charm_dir / 'metadata.yaml' + if metadata_path.is_file(): + charm_metadata = yaml.safe_load(metadata_path.read_text()) + self._charm_dir = charm_dir + + # Use default metadata if metadata is not found if charm_metadata is None: - metadata_path = charm_dir / 'metadata.yaml' - if metadata_path.is_file(): - charm_metadata = metadata_path.read_text() - self._charm_dir = charm_dir - else: - # The simplest of metadata that the framework can support - charm_metadata = 'name: test-charm' - elif isinstance(charm_metadata, str): - charm_metadata = dedent(charm_metadata) - - if action_metadata is None: - actions_path = charm_dir / 'actions.yaml' - if actions_path.is_file(): - action_metadata = actions_path.read_text() - self._charm_dir = charm_dir - elif isinstance(action_metadata, str): - action_metadata = dedent(action_metadata) - - return CharmMeta.from_yaml(charm_metadata, action_metadata) - - def _get_config(self, charm_config: Optional['YAMLStringOrFile']): + charm_metadata = {"name": "test-charm"} + + action_metadata: Optional[Dict[str, Any]] = None + # Load actions from parameters if provided + if action_metadata_yaml is not None: + if isinstance(action_metadata_yaml, str): + action_metadata_yaml = dedent(action_metadata_yaml) + action_metadata = yaml.safe_load(action_metadata_yaml) + else: + # Check charmcraft.yaml for actions if no actions are provided + if charmcraft_metadata is not None and "actions" in charmcraft_metadata: + action_metadata = charmcraft_metadata["actions"] + + # Still no actions, check actions.yaml + if action_metadata is None: + actions_path = charm_dir / 'actions.yaml' + if actions_path.is_file(): + action_metadata = yaml.safe_load(actions_path.read_text()) + self._charm_dir = charm_dir + + return CharmMeta(charm_metadata, action_metadata) + + def _get_config(self, charm_config_yaml: Optional['YAMLStringOrFile']): """If the user passed a config to Harness, use it. - Otherwise, attempt to load one from charm_dir/config.yaml. + Otherwise try to load config from ``/charmcraft.yaml`` first, then + ``/config.yaml`` if charmcraft.yaml does not include config. """ filename = inspect.getfile(self._charm_cls) charm_dir = pathlib.Path(filename).parents[1] + config: Optional[Dict[str, Any]] = None - if charm_config is None: - config_path = charm_dir / 'config.yaml' - if config_path.is_file(): - charm_config = config_path.read_text() - self._charm_dir = charm_dir - else: - # The simplest of config that the framework can support - charm_config = '{}' - elif isinstance(charm_config, str): - charm_config = dedent(charm_config) - - assert isinstance(charm_config, str) # type guard - config = yaml.safe_load(charm_config) + # Load config from parameters if provided + if charm_config_yaml is not None: + if isinstance(charm_config_yaml, str): + charm_config_yaml = dedent(charm_config_yaml) + config = yaml.safe_load(charm_config_yaml) + else: + # Check charmcraft.yaml for config if no config is provided + charmcraft_meta = charm_dir / "charmcraft.yaml" + if charmcraft_meta.is_file(): + charmcraft_metadata: Dict[str, Any] = yaml.safe_load(charmcraft_meta.read_text()) + config = charmcraft_metadata.get("config") + + # Still no config, check config.yaml + if config is None: + config_path = charm_dir / 'config.yaml' + if config_path.is_file(): + config = yaml.safe_load(config_path.read_text()) + self._charm_dir = charm_dir + + # Use default config if config is not found + if config is None: + config = {} if not isinstance(config, dict): raise TypeError(config) diff --git a/test/test_testing.py b/test/test_testing.py index f7c3b1d82..e7e07941b 100644 --- a/test/test_testing.py +++ b/test/test_testing.py @@ -1262,6 +1262,31 @@ def test_metadata_from_directory(self): # The charm_dir also gets set self.assertEqual(harness.framework.charm_dir, tmp) + def test_metadata_from_directory_charmcraft_yaml(self): + tmp = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, tmp) + charmcraft_filename = tmp / 'charmcraft.yaml' + charmcraft_filename.write_text(textwrap.dedent(''' + type: charm + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + name: my-charm + requires: + db: + interface: pgsql + ''')) + harness = self._get_dummy_charm_harness(tmp) + harness.begin() + self.assertEqual(list(harness.model.relations), ['db']) + # The charm_dir also gets set + self.assertEqual(harness.framework.charm_dir, tmp) + def test_config_from_directory(self): tmp = pathlib.Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, str(tmp)) @@ -1302,6 +1327,34 @@ def test_config_from_directory(self): self.assertIsNone(harness._backend._config._defaults['opt_null']) self.assertIsNone(harness._backend._config._defaults['opt_no_default']) + def test_config_from_directory_charmcraft_yaml(self): + tmp = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, tmp) + charmcraft_filename = tmp / 'charmcraft.yaml' + charmcraft_filename.write_text(textwrap.dedent(''' + type: charm + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + config: + options: + opt_str: + type: string + default: "val" + opt_int: + type: int + default: 1 + ''')) + harness = self._get_dummy_charm_harness(tmp) + self.assertEqual(harness.model.config['opt_str'], 'val') + self.assertEqual(harness.model.config['opt_int'], 1) + self.assertIsInstance(harness.model.config['opt_int'], int) + def test_set_model_name(self): harness = ops.testing.Harness(ops.CharmBase, meta=''' name: test-charm @@ -1721,6 +1774,30 @@ def test_actions_from_directory(self): # The charm_dir also gets set self.assertEqual(harness.framework.charm_dir, tmp) + def test_actions_from_directory_charmcraft_yaml(self): + tmp = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, tmp) + charmcraft_filename = tmp / 'charmcraft.yaml' + charmcraft_filename.write_text(textwrap.dedent(''' + type: charm + bases: + - build-on: + - name: ubuntu + channel: "22.04" + run-on: + - name: ubuntu + channel: "22.04" + + actions: + test: + description: a dummy action + ''')) + harness = self._get_dummy_charm_harness(tmp) + harness.begin() + self.assertEqual(list(harness.framework.meta.actions), ['test']) + # The charm_dir also gets set + self.assertEqual(harness.framework.charm_dir, tmp) + def _get_dummy_charm_harness(self, tmp): self._write_dummy_charm(tmp) charm_mod = importlib.import_module('testcharm')