mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-07-17 15:10:44 -07:00
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:
@@ -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="""" />
|
||||
<option name="_new_parameters" value="""" />
|
||||
<option name="_new_additionalArguments" value=""$PROJECT_DIR$/worlds -q --continue-on-collection-errors"" />
|
||||
<option name="_new_target" value=""$PROJECT_DIR$/test"" />
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
+31
@@ -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")
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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]]
|
||||
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()},
|
||||
|
||||
Reference in New Issue
Block a user