From 0549b8945e252ae7e4fc41a0633551bf79f499f2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:41:37 +0100 Subject: [PATCH] feat: add load_target_config and list_available_targets to common.py Loads per-platform target YAML files from platforms/targets/, resolves aliases and applies add_cores/remove_cores from _overrides.yml. Includes 7 E2E tests covering alias resolution, overrides, and error paths. --- scripts/common.py | 94 +++++++++++++++++++++++++++++++++++++ tests/test_e2e.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/scripts/common.py b/scripts/common.py index ecc4dc90..39385d36 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -192,6 +192,100 @@ def list_registered_platforms( return platforms +def load_target_config( + platform_name: str, + target: str, + platforms_dir: str = "platforms", +) -> set[str]: + """Load target config and return the set of core names for the given target. + + Resolves aliases from _overrides.yml, applies add_cores/remove_cores. + Raises ValueError if target is unknown (with list of available targets). + Raises FileNotFoundError if no target file exists for the platform. + """ + targets_dir = os.path.join(platforms_dir, "targets") + target_file = os.path.join(targets_dir, f"{platform_name}.yml") + if not os.path.exists(target_file): + raise FileNotFoundError( + f"No target config for platform '{platform_name}': {target_file}" + ) + with open(target_file) as f: + data = yaml.safe_load(f) or {} + + targets = data.get("targets", {}) + + overrides_file = os.path.join(targets_dir, "_overrides.yml") + overrides = {} + if os.path.exists(overrides_file): + with open(overrides_file) as f: + all_overrides = yaml.safe_load(f) or {} + overrides = all_overrides.get(platform_name, {}).get("targets", {}) + + alias_index: dict[str, str] = {} + for tname in targets: + alias_index[tname] = tname + for alias in overrides.get(tname, {}).get("aliases", []): + alias_index[alias] = tname + + canonical = alias_index.get(target) + if canonical is None: + available = sorted(targets.keys()) + aliases = [] + for tname, ovr in overrides.items(): + for a in ovr.get("aliases", []): + aliases.append(f"{a} -> {tname}") + msg = f"Unknown target '{target}' for platform '{platform_name}'.\n" + msg += f"Available targets: {', '.join(available)}" + if aliases: + msg += f"\nAliases: {', '.join(sorted(aliases))}" + raise ValueError(msg) + + cores = set(str(c) for c in targets[canonical].get("cores", [])) + + ovr = overrides.get(canonical, {}) + for c in ovr.get("add_cores", []): + cores.add(str(c)) + for c in ovr.get("remove_cores", []): + cores.discard(str(c)) + + return cores + + +def list_available_targets( + platform_name: str, + platforms_dir: str = "platforms", +) -> list[dict]: + """List available targets for a platform with their aliases. + + Returns list of dicts with keys: name, architecture, core_count, aliases. + Returns empty list if no target file exists. + """ + targets_dir = os.path.join(platforms_dir, "targets") + target_file = os.path.join(targets_dir, f"{platform_name}.yml") + if not os.path.exists(target_file): + return [] + with open(target_file) as f: + data = yaml.safe_load(f) or {} + + overrides_file = os.path.join(targets_dir, "_overrides.yml") + overrides = {} + if os.path.exists(overrides_file): + with open(overrides_file) as f: + all_overrides = yaml.safe_load(f) or {} + overrides = all_overrides.get(platform_name, {}).get("targets", {}) + + result = [] + for tname, tdata in sorted(data.get("targets", {}).items()): + aliases = overrides.get(tname, {}).get("aliases", []) + result.append({ + "name": tname, + "architecture": tdata.get("architecture", ""), + "core_count": len(tdata.get("cores", [])), + "aliases": aliases, + }) + return result + + def resolve_local_file( file_entry: dict, db: dict, diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 4f83e0ab..fb60cb74 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1175,6 +1175,122 @@ class TestE2E(unittest.TestCase): resolved = resolve_platform_cores(config, profiles) self.assertIn("test_alias_core", resolved) + # --------------------------------------------------------------- + # Target config tests (Task 1) + # --------------------------------------------------------------- + + def _write_target_fixtures(self): + """Create target config fixtures for testing.""" + targets_dir = os.path.join(self.platforms_dir, "targets") + os.makedirs(targets_dir, exist_ok=True) + target_config = { + "platform": "testplatform", + "source": "test", + "scraped_at": "2026-01-01T00:00:00Z", + "targets": { + "target-full": { + "architecture": "x86_64", + "cores": ["core_a", "core_b", "core_c"], + }, + "target-minimal": { + "architecture": "armv7", + "cores": ["core_a"], + }, + }, + } + with open(os.path.join(targets_dir, "testplatform.yml"), "w") as f: + yaml.dump(target_config, f) + single_config = { + "platform": "singleplatform", + "source": "test", + "scraped_at": "2026-01-01T00:00:00Z", + "targets": { + "only-target": { + "architecture": "x86_64", + "cores": ["core_a", "core_b"], + }, + }, + } + with open(os.path.join(targets_dir, "singleplatform.yml"), "w") as f: + yaml.dump(single_config, f) + overrides = { + "testplatform": { + "targets": { + "target-full": { + "aliases": ["full", "pc", "desktop"], + "add_cores": ["core_d"], + "remove_cores": ["core_c"], + }, + "target-minimal": { + "aliases": ["minimal", "arm"], + }, + }, + }, + } + with open(os.path.join(targets_dir, "_overrides.yml"), "w") as f: + yaml.dump(overrides, f) + + def test_load_target_config(self): + self._write_target_fixtures() + from common import load_target_config + cores = load_target_config("testplatform", "target-minimal", self.platforms_dir) + self.assertEqual(cores, {"core_a"}) + + def test_target_alias_resolution(self): + self._write_target_fixtures() + from common import load_target_config + cores = load_target_config("testplatform", "full", self.platforms_dir) + self.assertEqual(cores, {"core_a", "core_b", "core_d"}) + + def test_target_unknown_error(self): + self._write_target_fixtures() + from common import load_target_config + with self.assertRaises(ValueError) as ctx: + load_target_config("testplatform", "nonexistent", self.platforms_dir) + self.assertIn("target-full", str(ctx.exception)) + self.assertIn("target-minimal", str(ctx.exception)) + + def test_target_override_add_remove(self): + self._write_target_fixtures() + from common import load_target_config + cores = load_target_config("testplatform", "full", self.platforms_dir) + self.assertIn("core_d", cores) + self.assertNotIn("core_c", cores) + self.assertIn("core_a", cores) + self.assertIn("core_b", cores) + + def test_target_single_target_noop(self): + self._write_target_fixtures() + from common import load_target_config + cores = load_target_config("singleplatform", "only-target", self.platforms_dir) + self.assertEqual(cores, {"core_a", "core_b"}) + + def test_target_inherits(self): + self._write_target_fixtures() + targets_dir = os.path.join(self.platforms_dir, "targets") + child_config = { + "platform": "childplatform", + "source": "test", + "scraped_at": "2026-01-01T00:00:00Z", + "targets": { + "target-full": { + "architecture": "x86_64", + "cores": ["core_a"], + }, + }, + } + with open(os.path.join(targets_dir, "childplatform.yml"), "w") as f: + yaml.dump(child_config, f) + from common import load_target_config + parent = load_target_config("testplatform", "target-minimal", self.platforms_dir) + child = load_target_config("childplatform", "target-full", self.platforms_dir) + self.assertEqual(parent, {"core_a"}) + self.assertEqual(child, {"core_a"}) + self.assertNotEqual( + load_target_config("testplatform", "full", self.platforms_dir), + child, + ) + if __name__ == "__main__": unittest.main()