diff --git a/cli/build/defaults/applications.py b/cli/build/defaults/applications.py index d9c12d4c..07dcdd28 100644 --- a/cli/build/defaults/applications.py +++ b/cli/build/defaults/applications.py @@ -53,7 +53,8 @@ class DefaultsGenerator: continue if not config_file.exists(): - self.log(f"Skipping {role_name}: config/main.yml missing") + self.log(f"Config missing for {role_name}, adding empty defaults for '{application_id}'") + result["defaults_applications"][application_id] = {} continue config_data = load_yaml_file(config_file) @@ -106,4 +107,4 @@ if __name__ == "__main__": roles_dir = (cwd / args.roles_dir).resolve() output_file = (cwd / args.output_file).resolve() - DefaultsGenerator(roles_dir, output_file, args.verbose, args.timeout).run() \ No newline at end of file + DefaultsGenerator(roles_dir, output_file, args.verbose, args.timeout).run() diff --git a/roles/drv-msi-keyboard-color/README.md b/roles/drv-msi-keyboard-color/README.md index 8182f9e5..ce17b542 100644 --- a/roles/drv-msi-keyboard-color/README.md +++ b/roles/drv-msi-keyboard-color/README.md @@ -18,20 +18,6 @@ vendor_and_product_id: "" The `vendor_and_product_id` variable is required and should be set to the vendor and product ID of the MSI laptop. -## Dependencies - -- `sys-pgm-aur` - -## Example Playbook - -```yaml -- hosts: all - roles: - - keyboard-color - vars: - vendor_and_product_id: "your_vendor_and_product_id" -``` - ## Author This role was created by [Kevin Veen-Birkenbach](https://github.com/kevinveenbirkenbach). diff --git a/roles/drv-msi-keyboard-color/Todo.md b/roles/drv-msi-keyboard-color/Todo.md new file mode 100644 index 00000000..9924b132 --- /dev/null +++ b/roles/drv-msi-keyboard-color/Todo.md @@ -0,0 +1,2 @@ +# Todo +- Implement schema \ No newline at end of file diff --git a/roles/drv-msi-keyboard-color/schema/main.yml b/roles/drv-msi-keyboard-color/schema/main.yml new file mode 100644 index 00000000..2673f7cc --- /dev/null +++ b/roles/drv-msi-keyboard-color/schema/main.yml @@ -0,0 +1 @@ +vendor_and_product_id: "" # @todo schema needs to be implemented \ No newline at end of file diff --git a/roles/drv-msi-keyboard-color/vars/main.yml b/roles/drv-msi-keyboard-color/vars/main.yml index 8d9961e1..044d5ae0 100644 --- a/roles/drv-msi-keyboard-color/vars/main.yml +++ b/roles/drv-msi-keyboard-color/vars/main.yml @@ -1 +1,2 @@ -application_id: drv-msi-keyboard-color +application_id: drv-msi-keyboard-color +vendor_and_product_id: "{{ applications | get_app_conf(application_id, 'vendor_and_product_id') }}" diff --git a/tests/integration/test_get_app_conf_paths.py b/tests/integration/test_get_app_conf_paths.py index 71850534..9b5cc7af 100644 --- a/tests/integration/test_get_app_conf_paths.py +++ b/tests/integration/test_get_app_conf_paths.py @@ -7,7 +7,7 @@ from pathlib import Path import yaml # requires PyYAML from filter_plugins.get_role import get_role - +from filter_plugins.get_app_conf import get_app_conf, ConfigEntryNotSetError class TestGetAppConfPaths(unittest.TestCase): @classmethod @@ -88,12 +88,19 @@ class TestGetAppConfPaths(unittest.TestCase): cur = cur[k] def test_literal_paths(self): - # Check each literal path exists + # Check each literal path exists or is allowed by schema for app_id, paths in self.literal_paths.items(): with self.subTest(app=app_id): self.assertIn(app_id, self.defaults_app, f"App '{app_id}' missing in defaults_applications") for dotted, occs in paths.items(): with self.subTest(path=dotted): + try: + # will raise ConfigEntryNotSetError if defined in schema but not set + get_app_conf(self.defaults_app, app_id, dotted, strict=True) + except ConfigEntryNotSetError: + # defined in schema but not set: acceptable + continue + # otherwise, perform static validation self._validate(app_id, dotted, occs) def test_variable_paths(self): @@ -103,6 +110,13 @@ class TestGetAppConfPaths(unittest.TestCase): for dotted, occs in self.variable_paths.items(): with self.subTest(path=dotted): found = False + # schema-defined entries: acceptable if defined in any role schema + for schema in self.role_schemas.values(): + if isinstance(schema, dict) and dotted in schema: + found = True + break + if found: + continue # credentials.*: zuerst in defaults_applications prüfen, dann im Schema if dotted.startswith('credentials.'): key = dotted.split('.', 1)[1] @@ -182,4 +196,4 @@ class TestGetAppConfPaths(unittest.TestCase): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/tests/unit/cli/generate/defaults/test_applications.py b/tests/unit/cli/generate/defaults/test_applications.py index aa791fd0..3418ec0f 100644 --- a/tests/unit/cli/generate/defaults/test_applications.py +++ b/tests/unit/cli/generate/defaults/test_applications.py @@ -50,6 +50,61 @@ class TestGenerateDefaultApplications(unittest.TestCase): self.assertIn("testapp", data["defaults_applications"]) self.assertEqual(data["defaults_applications"]["testapp"]["foo"], "bar") self.assertEqual(data["defaults_applications"]["testapp"]["baz"], 123) + + def test_missing_config_adds_empty_defaults(self): + """ + If a role has vars/main.yml but no config/main.yml, + the generator should still create an entry with an empty dict. + """ + # Create a role with vars/main.yml but without config/main.yml + role_no_config = self.roles_dir / "role-no-config" + (role_no_config / "vars").mkdir(parents=True) + (role_no_config / "vars" / "main.yml").write_text("application_id: noconfigapp\n") + + # Run the generator + result = subprocess.run( + ["python3", str(self.script_path), + "--roles-dir", str(self.roles_dir), + "--output-file", str(self.output_file)], + capture_output=True, text=True + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + + # Verify the output YAML + data = yaml.safe_load(self.output_file.read_text()) + apps = data.get("defaults_applications", {}) + + # The new application_id must exist and be an empty dict + self.assertIn("noconfigapp", apps) + self.assertEqual(apps["noconfigapp"], {}) + + def test_no_config_directory_adds_empty_defaults(self): + """ + If a role has vars/main.yml but no config directory at all, + the generator should still emit an empty-dict entry. + """ + # Create a role with vars/main.yml but do not create config/ at all + role_no_cfg_dir = self.roles_dir / "role-no-cfg-dir" + (role_no_cfg_dir / "vars").mkdir(parents=True) + (role_no_cfg_dir / "vars" / "main.yml").write_text("application_id: nocfgdirapp\n") + # Note: no config/ directory is created here + + # Run the generator again + result = subprocess.run( + ["python3", str(self.script_path), + "--roles-dir", str(self.roles_dir), + "--output-file", str(self.output_file)], + capture_output=True, text=True + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + + # Load and inspect the output + data = yaml.safe_load(self.output_file.read_text()) + apps = data.get("defaults_applications", {}) + + # Ensure that the application_id appears with an empty mapping + self.assertIn("nocfgdirapp", apps) + self.assertEqual(apps["nocfgdirapp"], {}) if __name__ == "__main__": diff --git a/tests/unit/filter_plugins/test_app_conf_extra_schema.py b/tests/unit/filter_plugins/test_app_conf_extra_schema.py new file mode 100644 index 00000000..d065438b --- /dev/null +++ b/tests/unit/filter_plugins/test_app_conf_extra_schema.py @@ -0,0 +1,71 @@ +import os +import tempfile +import unittest +import yaml +from pathlib import Path + +from filter_plugins.get_app_conf import get_app_conf, AppConfigKeyError, ConfigEntryNotSetError + +class TestGetAppConfFunctionality(unittest.TestCase): + def setUp(self): + # Create a temp directory to simulate roles and schema files + self.tempdir = tempfile.TemporaryDirectory() + self.roles_dir = Path(self.tempdir.name) / "roles" + self.roles_dir.mkdir() + + # application_id used in tests + self.app_id = "testapp" + # application dict missing the 'nested.key' + self.applications = {self.app_id: {}} + + # Create schema directory and file for testapp + schema_dir = self.roles_dir / self.app_id / "schema" + schema_dir.mkdir(parents=True) + schema_file = schema_dir / "main.yml" + # Define a nested key in schema + schema_data = { + 'nested': { + 'key': '' + } + } + with schema_file.open('w', encoding='utf-8') as f: + yaml.safe_dump(schema_data, f) + + # Change cwd so get_app_conf finds roles//schema/main.yml + self.orig_cwd = os.getcwd() + os.chdir(self.tempdir.name) + + def tearDown(self): + os.chdir(self.orig_cwd) + self.tempdir.cleanup() + + def test_config_entry_not_set_error_raised(self): + # Attempt to access a key defined in schema but not set in applications + with self.assertRaises(ConfigEntryNotSetError): + get_app_conf(self.applications, self.app_id, 'nested.key', strict=True) + + def test_strict_key_error_when_not_in_schema(self): + # Access a key not defined in schema nor in applications + with self.assertRaises(AppConfigKeyError): + get_app_conf(self.applications, self.app_id, 'undefined.path', strict=True) + + def test_non_strict_returns_default(self): + # Non-strict mode should return default instead of raising + default_val = 'fallback' + result = get_app_conf(self.applications, self.app_id, 'nested.key', strict=False, default=default_val) + self.assertEqual(result, default_val) + + def test_list_indexing_and_missing(self): + # Extend schema to define a list + schema_file = Path('roles') / self.app_id / 'schema' / 'main.yml' + schema_data = {'items': ['']} + with schema_file.open('w', encoding='utf-8') as f: + yaml.safe_dump(schema_data, f) + + # Insert list into applications but leave index out of range + self.applications[self.app_id]['items'] = [] + with self.assertRaises(AppConfigKeyError): + get_app_conf(self.applications, self.app_id, 'items[0]', strict=True) + +if __name__ == '__main__': + unittest.main()