From 3105320038a6cbeb0b443a09a8338da31e574deb Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:52:44 +0000 Subject: [PATCH] Test: check fields in world source manifest (#5558) * Test: check game in world manifest * Update test/general/test_world_manifest.py Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> * Test: rework finding expected manifest location * Test: fix doc comment * Test: fix wrong custom_worlds path in test_world_manifest Also simplifies the way we find ./worlds/. * Test: make test_world_manifest easier to extend * Test: check world_version in world manifest according to docs/apworld specification.md * Test: check no container version in source world manifest according what was added to docs/apworld specification.md in PR 5509 * Test: better assertion messages in test_world_manifest.py * Test: fix wording in world source manifest --------- Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com> --- test/general/test_world_manifest.py | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/general/test_world_manifest.py diff --git a/test/general/test_world_manifest.py b/test/general/test_world_manifest.py new file mode 100644 index 00000000..3aaa93b0 --- /dev/null +++ b/test/general/test_world_manifest.py @@ -0,0 +1,102 @@ +"""Check world sources' manifest files""" + +import json +import unittest +from pathlib import Path +from typing import Any, ClassVar + +import test +from Utils import home_path, local_path +from worlds.AutoWorld import AutoWorldRegister +from ..param import classvar_matrix + + +test_path = Path(test.__file__).parent +worlds_paths = [ + Path(local_path("worlds")), + Path(local_path("custom_worlds")), + Path(home_path("worlds")), + Path(home_path("custom_worlds")), +] + +# Only check source folders for now. Zip validation should probably be in the loader and/or installer. +source_world_names = [ + k + for k, v in AutoWorldRegister.world_types.items() + if not v.zip_path and not Path(v.__file__).is_relative_to(test_path) +] + + +def get_source_world_manifest_path(game: str) -> Path | None: + """Get path of archipelago.json in the world's root folder from game name.""" + # TODO: add a feature to AutoWorld that makes this less annoying + world_type = AutoWorldRegister.world_types[game] + world_type_path = Path(world_type.__file__) + for worlds_path in worlds_paths: + if world_type_path.is_relative_to(worlds_path): + world_root = worlds_path / world_type_path.relative_to(worlds_path).parents[0] + manifest_path = world_root / "archipelago.json" + return manifest_path if manifest_path.exists() else None + assert False, f"{world_type_path} not found in any worlds path" + + +# TODO: remove the filter once manifests are mandatory. +@classvar_matrix(game=filter(get_source_world_manifest_path, source_world_names)) +class TestWorldManifest(unittest.TestCase): + game: ClassVar[str] + manifest: ClassVar[dict[str, Any]] + + @classmethod + def setUpClass(cls) -> None: + world_type = AutoWorldRegister.world_types[cls.game] + assert world_type.game == cls.game + manifest_path = get_source_world_manifest_path(cls.game) + assert manifest_path # make mypy happy + with manifest_path.open("r", encoding="utf-8") as f: + cls.manifest = json.load(f) + + def test_game(self) -> None: + """Test that 'game' will be correctly defined when generating APWorld manifest from source.""" + self.assertIn( + "game", + self.manifest, + f"archipelago.json manifest exists for {self.game} but does not contain 'game'", + ) + self.assertEqual( + self.manifest["game"], + self.game, + f"archipelago.json manifest for {self.game} specifies wrong game '{self.manifest['game']}'", + ) + + def test_world_version(self) -> None: + """Test that world_version matches the requirements in apworld specification.md""" + if "world_version" in self.manifest: + world_version: str = self.manifest["world_version"] + self.assertIsInstance( + world_version, + str, + f"world_version in archipelago.json for '{self.game}' has to be string if provided.", + ) + parts = world_version.split(".") + self.assertEqual( + len(parts), + 3, + f"world_version in archipelago.json for '{self.game}' has to be in the form of 'major.minor.build'.", + ) + for part in parts: + self.assertTrue( + part.isdigit(), + f"world_version in archipelago.json for '{self.game}' may only contain numbers.", + ) + + def test_no_container_version(self) -> None: + self.assertNotIn( + "version", + self.manifest, + f"archipelago.json for '{self.game}' must not define 'version', see apworld specification.md.", + ) + self.assertNotIn( + "compatible_version", + self.manifest, + f"archipelago.json for '{self.game}' must not define 'compatible_version', see apworld specification.md.", + )