diff --git a/.run/APQuest Tests.run.xml b/.run/APQuest Tests.run.xml
new file mode 100644
index 0000000000..4a735f2381
--- /dev/null
+++ b/.run/APQuest Tests.run.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000000..f884bb41e1
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,31 @@
+import os
+
+
+def pytest_configure(config):
+ # AP_TEST_WORLDS implies `-m world` unless an explicit -m was given; the scoping itself lives in
+ # worlds/__init__
+ if os.environ.get("AP_TEST_WORLDS") and not config.option.markexpr:
+ config.option.markexpr = "world"
+
+
+def pytest_ignore_collect(collection_path, config):
+ # skip worlds//... for any world not named, so other worlds are never imported
+ env = os.environ.get("AP_TEST_WORLDS")
+ if not env:
+ return None
+ selected = {name.strip() for name in env.split(",") if name.strip()}
+ parts = collection_path.parts
+ if "worlds" in parts:
+ i = parts.index("worlds")
+ if i + 1 < len(parts) and parts[i + 1] not in selected:
+ return True
+ return None
+
+
+def pytest_collection_modifyitems(items):
+ # mark for `-m world`: classes with `world_relevant = True`, plus anything under worlds/ (nodeid is
+ # always "/"-separated and relative to rootdir)
+ for item in items:
+ if getattr(getattr(item, "cls", None), "world_relevant", False) or \
+ item.nodeid.split("/", 1)[0] == "worlds":
+ item.add_marker("world")
diff --git a/docs/tests.md b/docs/tests.md
index 3c2956db93..95f2abd634 100644
--- a/docs/tests.md
+++ b/docs/tests.md
@@ -137,10 +137,26 @@ tests folder within your world.
You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window
next to the run and debug buttons.
+To run the suite scoped to a single world, use the shared **APQuest Tests** run configuration in the dropdown.
+To test your own world, duplicate it in *Edit Configurations…* and change the `AP_TEST_WORLDS` environment
+variable to your world's folder name.
+
#### Running Tests without Pycharm
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
+#### Running Tests for Specific Worlds
+
+Set the `AP_TEST_WORLDS` environment variable to a comma-separated list of world **folder** names to scope a run
+to just those worlds:
+
+```
+AP_TEST_WORLDS=apquest pytest
+```
+
+Pass several worlds with a comma, e.g. `AP_TEST_WORLDS=apquest,pokemon_emerald`. Add
+`--continue-on-collection-errors` if your environment is missing the webhost requirements (`flask`, etc.).
+
#### Running Tests Multithreaded
pytest can run multiple test runners in parallel with the pytest-xdist extension.
diff --git a/pytest.ini b/pytest.ini
index f050d58b70..a447573bea 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -5,3 +5,5 @@ python_functions = test
testpaths =
test
worlds
+markers =
+ world: general tests that iterate over every registered world (scope with AP_TEST_WORLDS)
diff --git a/test/bases.py b/test/bases.py
index 19b19bea67..999865a74b 100644
--- a/test/bases.py
+++ b/test/bases.py
@@ -202,7 +202,7 @@ class WorldTestBase(unittest.TestCase):
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
- state = self.multiworld.get_all_state(False)
+ state = self.multiworld.get_all_state()
for location in self.multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name):
reachable = location.can_reach(state)
diff --git a/test/benchmark/locations.py b/test/benchmark/locations.py
index 245dcb604e..f7c1775225 100644
--- a/test/benchmark/locations.py
+++ b/test/benchmark/locations.py
@@ -88,7 +88,7 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
if not locations:
continue
- all_state = multiworld.get_all_state(False)
+ all_state = multiworld.get_all_state()
for location in locations:
time_taken = self.location_test(location, multiworld.state, "empty_state")
summary_data["empty_state"][location.name] = time_taken
diff --git a/test/general/__init__.py b/test/general/__init__.py
index 53f77a516a..dbe6a17885 100644
--- a/test/general/__init__.py
+++ b/test/general/__init__.py
@@ -70,10 +70,12 @@ def setup_multiworld(worlds: list[type[World]] | type[World], steps: tuple[str,
class TestWebWorld(WebWorld):
+ __test__ = False # World subclass, not a test case; opt out of pytest.ini's python_classes = Test
tutorials = []
class TestWorld(World):
+ __test__ = False # World subclass, not a test case; opt out of pytest.ini's python_classes = Test
game = f"Test Game"
item_name_to_id = {}
location_name_to_id = {}
diff --git a/test/general/test_entrances.py b/test/general/test_entrances.py
index 79025534ac..effd932a37 100644
--- a/test/general/test_entrances.py
+++ b/test/general/test_entrances.py
@@ -4,6 +4,8 @@ from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
+ world_relevant = True
+
def test_entrance_connection_steps(self):
"""Tests that Entrances are connected and not changed after connect_entrances."""
def get_entrance_name_to_source_and_target_dict(world: World):
@@ -15,7 +17,7 @@ class TestBase(unittest.TestCase):
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
additional_steps = ("generate_basic", "pre_fill")
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
@@ -42,7 +44,7 @@ class TestBase(unittest.TestCase):
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
diff --git a/test/general/test_groups.py b/test/general/test_groups.py
index 486d3311fa..7a53d96886 100644
--- a/test/general/test_groups.py
+++ b/test/general/test_groups.py
@@ -4,11 +4,13 @@ from worlds.AutoWorld import AutoWorldRegister
class TestNameGroups(TestCase):
+ world_relevant = True
+
def test_item_name_groups_not_empty(self) -> None:
"""
Test that there are no empty item name groups, which is likely a bug.
"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.item_id_to_name:
continue # ignore worlds without items
with self.subTest(game=game_name):
@@ -19,7 +21,7 @@ class TestNameGroups(TestCase):
"""
Test that there are no empty location name groups, which is likely a bug.
"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.location_id_to_name:
continue # ignore worlds without locations
with self.subTest(game=game_name):
diff --git a/test/general/test_ids.py b/test/general/test_ids.py
index 08b4d0aa49..202dabfb26 100644
--- a/test/general/test_ids.py
+++ b/test/general/test_ids.py
@@ -7,23 +7,25 @@ from . import setup_solo_multiworld
class TestIDs(unittest.TestCase):
+ world_relevant = True
+
def test_range_items(self):
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
for item_id in world_type.item_id_to_name:
self.assertLess(item_id, 2**53)
def test_range_locations(self):
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
for location_id in world_type.location_id_to_name:
self.assertLess(location_id, 2**53)
def test_reserved_items(self):
"""negative item IDs are reserved to the special "Archipelago" world."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
if gamename == "Archipelago":
for item_id in world_type.item_id_to_name:
@@ -34,7 +36,7 @@ class TestIDs(unittest.TestCase):
def test_reserved_locations(self):
"""negative location IDs are reserved to the special "Archipelago" world."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
if gamename == "Archipelago":
for location_id in world_type.location_id_to_name:
@@ -45,7 +47,7 @@ class TestIDs(unittest.TestCase):
def test_duplicate_item_ids(self):
"""Test that a game doesn't have item id overlap within its own datapackage"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
len_item_id_to_name = len(world_type.item_id_to_name)
len_item_name_to_id = len(world_type.item_name_to_id)
@@ -64,7 +66,7 @@ class TestIDs(unittest.TestCase):
def test_duplicate_location_ids(self):
"""Test that a game doesn't have location id overlap within its own datapackage"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
len_location_id_to_name = len(world_type.location_id_to_name)
len_location_name_to_id = len(world_type.location_name_to_id)
@@ -83,7 +85,7 @@ class TestIDs(unittest.TestCase):
def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py
index 0906852201..a388b09265 100644
--- a/test/general/test_implemented.py
+++ b/test/general/test_implemented.py
@@ -8,9 +8,11 @@ from . import setup_solo_multiworld
class TestImplemented(unittest.TestCase):
+ world_relevant = True
+
def test_completion_condition(self):
"""Ensure a completion condition is set that has requirements."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
with self.subTest(game_name):
multiworld = setup_solo_multiworld(world_type)
@@ -18,7 +20,7 @@ class TestImplemented(unittest.TestCase):
def test_entrance_parents(self):
"""Tests that the parents of created Entrances match the exiting Region."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
with self.subTest(game_name):
multiworld = setup_solo_multiworld(world_type)
@@ -28,7 +30,7 @@ class TestImplemented(unittest.TestCase):
def test_stage_methods(self):
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
with self.subTest(game_name):
for method in ("assert_generate",):
@@ -40,7 +42,7 @@ class TestImplemented(unittest.TestCase):
# has an await for generate_output which isn't being called
excluded_games = ("Ocarina of Time",)
worlds_to_test = {game: world
- for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
+ for game, world in AutoWorldRegister.testable_worlds.items() if game not in excluded_games}
for game_name, world_type in worlds_to_test.items():
multiworld = setup_solo_multiworld(world_type)
with self.subTest(game=game_name, seed=multiworld.seed):
@@ -58,12 +60,12 @@ class TestImplemented(unittest.TestCase):
def test_prefill_items(self):
"""Test that every world can reach every location from allstate before pre_fill."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
if gamename not in ("Archipelago", "Final Fantasy", "Test Game"):
with self.subTest(gamename):
multiworld = setup_solo_multiworld(world_type, ("generate_early", "create_regions", "create_items",
"set_rules", "connect_entrances", "generate_basic"))
- allstate = multiworld.get_all_state(False)
+ allstate = multiworld.get_all_state()
locations = multiworld.get_locations()
reachable = multiworld.get_reachable_locations(allstate)
unreachable = [location for location in locations if location not in reachable]
@@ -78,7 +80,7 @@ class TestImplemented(unittest.TestCase):
# Because the iteration order of blocked_connections in CollectionState.update_reachable_regions() is
# nondeterministic, this test may sometimes pass with the same seed even when there are missing indirect
# conditions.
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
multiworld = setup_solo_multiworld(world_type)
world = multiworld.get_game_worlds(game_name)[0]
if not world.explicit_indirect_conditions:
@@ -140,7 +142,7 @@ class TestImplemented(unittest.TestCase):
def test_no_items_or_locations_or_regions_submitted_in_init(self):
"""Test that worlds don't submit items/locations/regions to the multiworld in __init__"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, ())
self.assertEqual(len(multiworld.itempool), 0)
diff --git a/test/general/test_items.py b/test/general/test_items.py
index 9c300cf94e..8cc81e7b5a 100644
--- a/test/general/test_items.py
+++ b/test/general/test_items.py
@@ -11,9 +11,11 @@ from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
+ world_relevant = True
+
def test_create_item(self):
"""Test that a world can successfully create all items in its datapackage"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
proxy_world = multiworld.worlds[1]
for item_name in world_type.item_name_to_id:
@@ -53,7 +55,7 @@ class TestBase(unittest.TestCase):
"Yu-Gi-Oh! 2006":
{"Campaign Boss Beaten"}
}
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game_name, game_name=game_name):
exclusions = exclusion_dict.get(game_name, frozenset())
for group_name, items in world_type.item_name_groups.items():
@@ -64,7 +66,7 @@ class TestBase(unittest.TestCase):
def test_item_name_group_conflict(self):
"""Test that all item name groups aren't also item names."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game_name, game_name=game_name):
for group_name in world_type.item_name_groups:
with self.subTest(group_name, group_name=group_name):
@@ -72,7 +74,7 @@ class TestBase(unittest.TestCase):
def test_item_count_equal_locations(self):
"""Test that by the pre_fill step under default settings, each game submits items == locations"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
self.assertEqual(
@@ -84,7 +86,7 @@ class TestBase(unittest.TestCase):
def test_items_in_datapackage(self):
"""Test that any created items in the itempool are in the datapackage"""
archipelago = AutoWorldRegister.world_types["Archipelago"]
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
for item in multiworld.itempool:
@@ -126,7 +128,7 @@ class TestBase(unittest.TestCase):
call_all(multiworld, "finalize_multiworld")
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Can generate with link replacement", game=game_name):
setup_link_multiworld(world_type, True)
with self.subTest("Can generate without link replacement", game=game_name):
@@ -138,7 +140,7 @@ class TestBase(unittest.TestCase):
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world
- for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
+ for game, world in AutoWorldRegister.testable_worlds.items() if game not in excluded_games}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
@@ -153,7 +155,7 @@ class TestBase(unittest.TestCase):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early",)
additional_steps = ("create_regions", "create_items", "set_rules", "connect_entrances", "generate_basic", "pre_fill")
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
local_items = multiworld.worlds[1].options.local_items.value.copy()
diff --git a/test/general/test_locations.py b/test/general/test_locations.py
index 77ae2602e5..378d420942 100644
--- a/test/general/test_locations.py
+++ b/test/general/test_locations.py
@@ -5,9 +5,11 @@ from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
+ world_relevant = True
+
def test_create_duplicate_locations(self):
"""Tests that no two Locations share a name or ID."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
multiworld = setup_solo_multiworld(world_type)
locations = Counter(location.name for location in multiworld.get_locations())
if locations:
@@ -22,7 +24,7 @@ class TestBase(unittest.TestCase):
def test_locations_in_datapackage(self):
"""Tests that created locations not filled before fill starts exist in the datapackage."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type)
locations = multiworld.get_unfilled_locations() # do unfilled locations to avoid Events
@@ -35,7 +37,7 @@ class TestBase(unittest.TestCase):
gen_steps = ("generate_early", "create_regions", "create_items")
excluded_games = ("Ocarina of Time", "Pokemon Red and Blue")
worlds_to_test = {game: world
- for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
+ for game, world in AutoWorldRegister.testable_worlds.items() if game not in excluded_games}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
@@ -68,7 +70,7 @@ class TestBase(unittest.TestCase):
def test_location_group(self):
"""Test that all location name groups contain valid locations and don't share names."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game_name, game_name=game_name):
for group_name, locations in world_type.location_name_groups.items():
with self.subTest(group_name, group_name=group_name):
diff --git a/test/general/test_memory.py b/test/general/test_memory.py
index a4b2f1bd25..38edeb6b51 100644
--- a/test/general/test_memory.py
+++ b/test/general/test_memory.py
@@ -6,12 +6,14 @@ from . import setup_solo_multiworld
class TestWorldMemory(unittest.TestCase):
+ world_relevant = True
+
def test_leak(self) -> None:
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
import gc
import weakref
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game creation", game_name=game_name):
weak = weakref.ref(setup_solo_multiworld(world_type))
refs[game_name] = weak
diff --git a/test/general/test_names.py b/test/general/test_names.py
index 8ad74a3354..2af8ee5a1a 100644
--- a/test/general/test_names.py
+++ b/test/general/test_names.py
@@ -3,9 +3,11 @@ from worlds.AutoWorld import AutoWorldRegister
class TestNames(unittest.TestCase):
+ world_relevant = True
+
def test_item_names_format(self) -> None:
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
for item_name in world_type.item_name_to_id:
self.assertFalse(item_name.isnumeric(),
@@ -13,7 +15,7 @@ class TestNames(unittest.TestCase):
def test_location_name_format(self) -> None:
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
for location_name in world_type.location_name_to_id:
self.assertFalse(location_name.isnumeric(),
diff --git a/test/general/test_options.py b/test/general/test_options.py
index 5d69b6820b..f015aff9a0 100644
--- a/test/general/test_options.py
+++ b/test/general/test_options.py
@@ -8,9 +8,11 @@ from worlds.AutoWorld import AutoWorldRegister
class TestOptions(unittest.TestCase):
+ world_relevant = True
+
def test_options_have_doc_string(self):
"""Test that submitted options have their own specified docstring"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
@@ -18,7 +20,7 @@ class TestOptions(unittest.TestCase):
def test_option_defaults(self):
"""Test that defaults for submitted options are valid."""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
@@ -41,14 +43,14 @@ class TestOptions(unittest.TestCase):
def test_options_are_not_set_by_world(self):
"""Test that options attribute is not already set"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=gamename):
self.assertFalse(hasattr(world_type, "options"),
f"Unexpected assignment to {world_type.__name__}.options!")
def test_duplicate_options(self) -> None:
"""Tests that a world doesn't reuse the same option class."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=game_name):
seen_options = set()
for option in world_type.options_dataclass.type_hints.values():
@@ -98,7 +100,7 @@ class TestOptions(unittest.TestCase):
def test_pickle_dumps_default(self):
"""Test that default option values can be pickled into database for WebHost generation"""
- for gamename, world_type in AutoWorldRegister.world_types.items():
+ for gamename, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
@@ -108,7 +110,7 @@ class TestOptions(unittest.TestCase):
def test_option_set_keys_random(self):
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if game_name not in ("Archipelago", "Super Metroid"):
for option_key, option in world_type.options_dataclass.type_hints.items():
if issubclass(option, OptionSet):
diff --git a/test/general/test_packages.py b/test/general/test_packages.py
index 1df6187ee0..804426e7d0 100644
--- a/test/general/test_packages.py
+++ b/test/general/test_packages.py
@@ -3,6 +3,8 @@ import os
class TestPackages(unittest.TestCase):
+ world_relevant = True
+
def test_packages_have_init(self):
"""Test that all world folders containing .py files also have a __init__.py file,
to indicate full package rather than namespace package."""
diff --git a/test/general/test_patches.py b/test/general/test_patches.py
index 09782b8d3d..ac1d0f9b13 100644
--- a/test/general/test_patches.py
+++ b/test/general/test_patches.py
@@ -4,6 +4,8 @@ from worlds.Files import AutoPatchRegister
class TestPatches(unittest.TestCase):
+ world_relevant = True
+
def test_patch_name_matches_game(self) -> None:
for game_name in AutoPatchRegister.patch_types:
with self.subTest(game=game_name):
diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py
index b45a2bdfc0..79ff3afb98 100644
--- a/test/general/test_reachability.py
+++ b/test/general/test_reachability.py
@@ -6,6 +6,7 @@ from . import setup_solo_multiworld, gen_steps
class TestBase(unittest.TestCase):
+ world_relevant = True
gen_steps = gen_steps
default_settings_unreachable_regions = {
@@ -45,11 +46,11 @@ class TestBase(unittest.TestCase):
def test_default_all_state_can_reach_everything(self):
"""Ensure all state can reach everything and complete the game with the defined options"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
- state = multiworld.get_all_state(False)
+ state = multiworld.get_all_state()
for location in multiworld.get_locations():
with self.subTest("Location should be reached", location=location.name):
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
@@ -67,7 +68,7 @@ class TestBase(unittest.TestCase):
def test_default_empty_state_can_reach_something(self):
"""Ensure empty state can reach at least one location with the defined options"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type)
state = CollectionState(multiworld)
diff --git a/test/general/test_requirements.py b/test/general/test_requirements.py
index 620d3d610a..5ab5b53675 100644
--- a/test/general/test_requirements.py
+++ b/test/general/test_requirements.py
@@ -3,6 +3,8 @@ import os
class TestBase(unittest.TestCase):
+ world_relevant = True
+
def test_requirements_file_ends_on_newline(self):
"""Test that all requirements files end on a newline"""
import Utils
diff --git a/test/general/test_settings.py b/test/general/test_settings.py
index 165d7982b5..2192d63075 100644
--- a/test/general/test_settings.py
+++ b/test/general/test_settings.py
@@ -5,11 +5,13 @@ from worlds.AutoWorld import AutoWorldRegister
class TestSettings(TestCase):
+ world_relevant = True
+
def test_settings_can_update(self) -> None:
"""
Test that world settings can update.
"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest(game=game_name):
if world_type.settings is not None:
assert isinstance(world_type.settings, Group)
diff --git a/test/general/test_state.py b/test/general/test_state.py
index 06c4046a69..9451077e6d 100644
--- a/test/general/test_state.py
+++ b/test/general/test_state.py
@@ -5,6 +5,7 @@ from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
+ world_relevant = True
gen_steps = (
"generate_early",
"create_regions",
@@ -20,10 +21,10 @@ class TestBase(unittest.TestCase):
def test_all_state_is_available(self):
"""Ensure all_state can be created at certain steps."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
for step in self.test_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
- self.assertTrue(multiworld.get_all_state(False, allow_partial_entrances=True))
+ self.assertTrue(multiworld.get_all_state(allow_partial_entrances=True))
diff --git a/test/general/test_world_manifest.py b/test/general/test_world_manifest.py
index 3aaa93b0ac..94953cac03 100644
--- a/test/general/test_world_manifest.py
+++ b/test/general/test_world_manifest.py
@@ -22,7 +22,7 @@ worlds_paths = [
# 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()
+ for k, v in AutoWorldRegister.testable_worlds.items()
if not v.zip_path and not Path(v.__file__).is_relative_to(test_path)
]
@@ -43,6 +43,7 @@ def get_source_world_manifest_path(game: str) -> Path | None:
# TODO: remove the filter once manifests are mandatory.
@classvar_matrix(game=filter(get_source_world_manifest_path, source_world_names))
class TestWorldManifest(unittest.TestCase):
+ world_relevant = True
game: ClassVar[str]
manifest: ClassVar[dict[str, Any]]
diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py
index d22013b4e0..f11efe65f6 100644
--- a/test/multiworld/test_multiworlds.py
+++ b/test/multiworld/test_multiworlds.py
@@ -65,13 +65,14 @@ class TestAllGamesMultiworld(MultiworldTestBase):
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
-@classvar_matrix(game=AutoWorldRegister.world_types.keys())
+@classvar_matrix(game=AutoWorldRegister.testable_worlds.keys())
class TestTwoPlayerMulti(MultiworldTestBase):
+ world_relevant = True
game: ClassVar[str]
def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate."""
- world_type = AutoWorldRegister.world_types[self.game]
+ world_type = AutoWorldRegister.testable_worlds[self.game]
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
diff --git a/test/webhost/test_descriptions.py b/test/webhost/test_descriptions.py
index 70f375b51c..0eed68728f 100644
--- a/test/webhost/test_descriptions.py
+++ b/test/webhost/test_descriptions.py
@@ -4,9 +4,11 @@ from worlds.AutoWorld import AutoWorldRegister
class TestWebDescriptions(unittest.TestCase):
+ world_relevant = True
+
def test_item_descriptions_have_valid_names(self) -> None:
"""Ensure all item descriptions match an item name or item group name"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
valid_names = world_type.item_names.union(world_type.item_name_groups)
for name in world_type.web.item_descriptions:
with self.subTest("Name should be valid", game=game_name, item=name):
@@ -15,7 +17,7 @@ class TestWebDescriptions(unittest.TestCase):
def test_location_descriptions_have_valid_names(self) -> None:
"""Ensure all location descriptions match a location name or location group name"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
valid_names = world_type.location_names.union(world_type.location_name_groups)
for name in world_type.web.location_descriptions:
with self.subTest("Name should be valid", game=game_name, location=name):
diff --git a/test/webhost/test_docs.py b/test/webhost/test_docs.py
index a178a7cbf0..02c0b51765 100644
--- a/test/webhost/test_docs.py
+++ b/test/webhost/test_docs.py
@@ -9,12 +9,14 @@ from worlds.AutoWorld import AutoWorldRegister
class TestDocs(unittest.TestCase):
+ world_relevant = True
+
@classmethod
def setUpClass(cls) -> None:
WebHost.copy_tutorials_files_to_static()
def test_has_tutorial(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
with self.subTest(game_name):
tutorials = world_type.web.tutorials
@@ -29,7 +31,7 @@ class TestDocs(unittest.TestCase):
)
def test_has_game_info(self):
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
if not world_type.hidden:
safe_name = secure_filename(game_name)
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name)
diff --git a/test/webhost/test_file_generation.py b/test/webhost/test_file_generation.py
index 7b14ac871b..e6c36e37ef 100644
--- a/test/webhost/test_file_generation.py
+++ b/test/webhost/test_file_generation.py
@@ -7,6 +7,8 @@ import WebHost
class TestFileGeneration(unittest.TestCase):
+ world_relevant = True
+
@classmethod
def setUpClass(cls) -> None:
cls.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py
index 7f05de5d11..f1568befd1 100644
--- a/test/webhost/test_option_presets.py
+++ b/test/webhost/test_option_presets.py
@@ -6,9 +6,11 @@ from Options import OptionCounter, NamedRange, NumericOption, OptionList, Option
class TestOptionPresets(unittest.TestCase):
+ world_relevant = True
+
def test_option_presets_have_valid_options(self):
"""Test that all predefined option presets are valid options."""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
presets = world_type.web.options_presets
for preset_name, preset in presets.items():
for option_name, option_value in preset.items():
@@ -38,7 +40,7 @@ class TestOptionPresets(unittest.TestCase):
"""Test that option preset values are not a special flavor of 'random' or use from_text to resolve another
value.
"""
- for game_name, world_type in AutoWorldRegister.world_types.items():
+ for game_name, world_type in AutoWorldRegister.testable_worlds.items():
presets = world_type.web.options_presets
for preset_name, preset in presets.items():
for option_name, option_value in preset.items():
diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py
index 4a4e3e07c1..8f6b77ffe8 100644
--- a/test/worlds/__init__.py
+++ b/test/worlds/__init__.py
@@ -35,7 +35,7 @@ def load_tests(loader: "TestLoader", standard_tests: "TestSuite", pattern: str):
folders = [os.path.join(os.path.split(world.__file__)[0], "test")
- for world in AutoWorldRegister.world_types.values()
+ for world in AutoWorldRegister.testable_worlds.values()
if fnmatch.fnmatch(world.__module__, world_glob)]
all_tests = [
diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py
index 468706a347..6f0d211c1a 100644
--- a/worlds/AutoWorld.py
+++ b/worlds/AutoWorld.py
@@ -28,7 +28,9 @@ class InvalidItemError(KeyError):
class AutoWorldRegister(type):
- world_types: Dict[str, Type[World]] = {}
+ world_types: dict[str, Type[World]] = {}
+ testable_worlds: dict[str, Type[World]] = world_types
+ """worlds under test; scoped to AP_TEST_WORLDS by worlds/__init__"""
__file__: str
zip_path: Optional[str]
settings_key: str
diff --git a/worlds/__init__.py b/worlds/__init__.py
index 8684b74249..c5fc00746d 100644
--- a/worlds/__init__.py
+++ b/worlds/__init__.py
@@ -74,6 +74,13 @@ class WorldSource:
return False
+# AP_TEST_WORLDS (pytest only) scopes auto-loading to the named worlds; these are always loaded for the
+# suite's fixtures but aren't themselves worlds under test.
+_SUITE_FIXTURE_WORLDS = {"generic", "apquest"}
+_test_worlds_env = os.environ.get("AP_TEST_WORLDS") if "pytest" in sys.modules else None
+_requested_worlds = {name.strip() for name in _test_worlds_env.split(",") if name.strip()} if _test_worlds_env else None
+test_worlds_filter = _requested_worlds | _SUITE_FIXTURE_WORLDS if _requested_worlds else None
+
# find potential world containers, currently folders and zip-importable .apworld's
world_sources: List[WorldSource] = []
for folder in (folder for folder in (user_folder, local_folder) if folder):
@@ -81,6 +88,8 @@ for folder in (folder for folder in (user_folder, local_folder) if folder):
for entry in os.scandir(folder):
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
if not entry.name.startswith(("_", ".")):
+ if test_worlds_filter is not None and Path(entry.name).stem not in test_worlds_filter:
+ continue
file_name = entry.name if relative else os.path.join(folder, entry.name)
if entry.is_dir():
if os.path.isfile(os.path.join(entry.path, '__init__.py')):
@@ -215,6 +224,16 @@ if apworlds:
del apworlds
+# snapshot the worlds under test (a copy, so tests reassigning world_types can't leak in), dropping the
+# force-loaded fixtures unless they were explicitly requested.
+if _requested_worlds:
+ AutoWorldRegister.testable_worlds = {
+ game: world for game, world in AutoWorldRegister.world_types.items()
+ if Path(world.__file__).parent.name in _requested_worlds
+ }
+else:
+ AutoWorldRegister.testable_worlds = dict(AutoWorldRegister.world_types)
+
# Build the data package for each game.
network_data_package: DataPackage = {
"games": {world_name: world.get_data_package_data() for world_name, world in AutoWorldRegister.world_types.items()},