Core: Allow running unit tests scoped to specific worlds (#6290)

This allows the unit tests to be run on specific worlds only, it works by using an environment variable AP_TEST_WORLDS which is a comma separated list of world directories.
This commit is contained in:
James White
2026-07-17 19:01:28 +01:00
committed by GitHub
parent c311685064
commit 9c80dc6257
31 changed files with 194 additions and 59 deletions
+25
View File
@@ -0,0 +1,25 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="APQuest Tests" type="tests" factoryName="py.test">
<module name="Archipelago" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="AP_TEST_WORLDS" value="apquest" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="DEBUG_JUST_MY_CODE" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="RUN_TOOL" value="" />
<option name="_new_keywords" value="&quot;&quot;" />
<option name="_new_parameters" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;$PROJECT_DIR$/worlds -q --continue-on-collection-errors&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/test&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>
+31
View File
@@ -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/<world>/... 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")
+16
View File
@@ -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.
+2
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+2
View File
@@ -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 = {}
+4 -2
View File
@@ -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, ())
+4 -2
View File
@@ -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):
+9 -7
View File
@@ -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)
+10 -8
View File
@@ -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)
+10 -8
View File
@@ -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()
+6 -4
View File
@@ -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):
+3 -1
View File
@@ -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
+4 -2
View File
@@ -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(),
+8 -6
View File
@@ -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):
+2
View File
@@ -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."""
+2
View File
@@ -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):
+4 -3
View File
@@ -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)
+2
View File
@@ -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
+3 -1
View File
@@ -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)
+3 -2
View File
@@ -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))
+2 -1
View File
@@ -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]]
+3 -2
View File
@@ -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
+4 -2
View File
@@ -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):
+4 -2
View File
@@ -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)
+2
View File
@@ -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")
+4 -2
View File
@@ -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():
+1 -1
View File
@@ -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 = [
+3 -1
View File
@@ -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
+19
View File
@@ -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()},