Test: Remove most dependencies on lttp (#5338)

* removes the last dependencies on lttp in tests

* removing test.bases.TestBase from docs as well

* rename bases

* move imports to bases
This commit is contained in:
qwint
2025-09-08 14:36:26 -05:00
committed by GitHub
parent 63f3512829
commit 17dad8313e
15 changed files with 172 additions and 178 deletions

View File

@@ -82,10 +82,10 @@ overridden. For more information on what methods are available to your class, ch
#### Alternatives to WorldTestBase #### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L16) or Unit tests can also be created using
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These [unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) directly. These may be useful
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for for generating a multiworld under very specific constraints without using the generic world setup, or for testing
testing portions of your code that can be tested without relying on a multiworld to be created first. portions of your code that can be tested without relying on a multiworld to be created first.
#### Parametrization #### Parametrization
@@ -102,8 +102,7 @@ for multiple inputs) the base test. Some important things to consider when attem
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all * Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly extra CPU time. Consider using `unittest.TestCase` directly or setting `WorldTestBase.run_default_tests` to False.
or setting `WorldTestBase.run_default_tests` to False.
#### Performance Considerations #### Performance Considerations

View File

@@ -9,98 +9,7 @@ from test.general import gen_steps
from worlds import AutoWorld from worlds import AutoWorld
from worlds.AutoWorld import World, call_all from worlds.AutoWorld import World, call_all
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item from BaseClasses import Location, MultiWorld, CollectionState, Item
from worlds.alttp.Items import item_factory
class TestBase(unittest.TestCase):
multiworld: MultiWorld
_state_cache = {}
def get_state(self, items):
if (self.multiworld, tuple(items)) in self._state_cache:
return self._state_cache[self.multiworld, tuple(items)]
state = CollectionState(self.multiworld)
for item in items:
item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True)
state.sweep_for_advancements()
state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state
def get_path(self, state, region):
def flist_to_iter(node):
while node:
value, node = node
yield value
from itertools import zip_longest
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
def run_location_tests(self, access_pool):
for i, (location, access, *item_pool) in enumerate(access_pool):
items = item_pool[0]
all_except = item_pool[1] if len(item_pool) > 1 else None
state = self._get_items(item_pool, all_except)
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
all_except=all_except, path=path, entry=i):
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
for missing_item in item_pool[0]:
with self.subTest(msg="Location reachable without required item", location=location,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
f"{missing_item} removed from: {item_pool}")
def run_entrance_tests(self, access_pool):
for i, (entrance, access, *item_pool) in enumerate(access_pool):
items = item_pool[0]
all_except = item_pool[1] if len(item_pool) > 1 else None
state = self._get_items(item_pool, all_except)
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
all_except=all_except, path=path, entry=i):
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
for missing_item in item_pool[0]:
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:
items = self.multiworld.itempool[:]
items = [item for item in items if
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
items.extend(item_factory(item_pool[0], self.multiworld.worlds[1]))
else:
items = item_factory(item_pool[0], self.multiworld.worlds[1])
return self.get_state(items)
def _get_items_partial(self, item_pool, missing_item):
new_items = item_pool[0].copy()
new_items.remove(missing_item)
items = item_factory(new_items, self.multiworld.worlds[1])
return self.get_state(items)
class WorldTestBase(unittest.TestCase): class WorldTestBase(unittest.TestCase):

View File

@@ -1,22 +0,0 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState
from worlds import AutoWorldRegister
class LTTPTestBase(unittest.TestCase):
def world_setup(self):
from worlds.alttp.Options import Medallion
self.multiworld = MultiWorld(1)
self.multiworld.game[1] = "A Link to the Past"
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(getattr(option, "default"))})
self.multiworld.set_options(args)
self.multiworld.state = CollectionState(self.multiworld)
self.world = self.multiworld.worlds[1]
# by default medallion access is randomized, for unittests we set it to vanilla
self.world.options.misery_mire_medallion.value = Medallion.option_ether
self.world.options.turtle_rock_medallion.value = Medallion.option_quake

113
worlds/alttp/test/bases.py Normal file
View File

@@ -0,0 +1,113 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld, CollectionState, ItemClassification
from worlds import AutoWorldRegister
from ..Items import item_factory
class TestBase(unittest.TestCase):
multiworld: MultiWorld
_state_cache = {}
def get_state(self, items):
if (self.multiworld, tuple(items)) in self._state_cache:
return self._state_cache[self.multiworld, tuple(items)]
state = CollectionState(self.multiworld)
for item in items:
item.classification = ItemClassification.progression
state.collect(item, prevent_sweep=True)
state.sweep_for_advancements()
state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state
def get_path(self, state, region):
def flist_to_iter(node):
while node:
value, node = node
yield value
from itertools import zip_longest
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
def run_location_tests(self, access_pool):
for i, (location, access, *item_pool) in enumerate(access_pool):
items = item_pool[0]
all_except = item_pool[1] if len(item_pool) > 1 else None
state = self._get_items(item_pool, all_except)
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
all_except=all_except, path=path, entry=i):
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
for missing_item in item_pool[0]:
with self.subTest(msg="Location reachable without required item", location=location,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
f"{missing_item} removed from: {item_pool}")
def run_entrance_tests(self, access_pool):
for i, (entrance, access, *item_pool) in enumerate(access_pool):
items = item_pool[0]
all_except = item_pool[1] if len(item_pool) > 1 else None
state = self._get_items(item_pool, all_except)
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
all_except=all_except, path=path, entry=i):
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
for missing_item in item_pool[0]:
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:
items = self.multiworld.itempool[:]
items = [item for item in items if
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
items.extend(item_factory(item_pool[0], self.multiworld.worlds[1]))
else:
items = item_factory(item_pool[0], self.multiworld.worlds[1])
return self.get_state(items)
def _get_items_partial(self, item_pool, missing_item):
new_items = item_pool[0].copy()
new_items.remove(missing_item)
items = item_factory(new_items, self.multiworld.worlds[1])
return self.get_state(items)
class LTTPTestBase(unittest.TestCase):
def world_setup(self):
from worlds.alttp.Options import Medallion
self.multiworld = MultiWorld(1)
self.multiworld.game[1] = "A Link to the Past"
self.multiworld.set_seed(None)
args = Namespace()
for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items():
setattr(args, name, {1: option.from_any(getattr(option, "default"))})
self.multiworld.set_options(args)
self.multiworld.state = CollectionState(self.multiworld)
self.world = self.multiworld.worlds[1]
# by default medallion access is randomized, for unittests we set it to vanilla
self.world.options.misery_mire_medallion.value = Medallion.option_ether
self.world.options.turtle_rock_medallion.value = Medallion.option_quake

View File

@@ -5,7 +5,7 @@ from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import item_factory from worlds.alttp.Items import item_factory
from worlds.alttp.Regions import create_regions from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.test import LTTPTestBase from worlds.alttp.test.bases import LTTPTestBase
class TestDungeon(LTTPTestBase): class TestDungeon(LTTPTestBase):

View File

@@ -1,13 +1,12 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from ...EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from ...InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from worlds.alttp.Regions import mark_light_world_regions from ...Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from ...Shops import create_shops
from test.bases import TestBase
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestInverted(TestBase, LTTPTestBase): class TestInverted(TestBase, LTTPTestBase):

View File

@@ -4,7 +4,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules from worlds.alttp.Rules import set_inverted_big_bomb_rules
from worlds.alttp.test import LTTPTestBase from worlds.alttp.test.bases import LTTPTestBase
class TestInvertedBombRules(LTTPTestBase): class TestInvertedBombRules(LTTPTestBase):

View File

@@ -1,14 +1,13 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from ...EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from ...InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from worlds.alttp.Options import GlitchesRequired from ...Options import GlitchesRequired
from worlds.alttp.Regions import mark_light_world_regions from ...Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from ...Shops import create_shops
from test.bases import TestBase
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestInvertedMinor(TestBase, LTTPTestBase): class TestInvertedMinor(TestBase, LTTPTestBase):

View File

@@ -1,14 +1,13 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from ...EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from ...InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from worlds.alttp.Options import GlitchesRequired from ...Options import GlitchesRequired
from worlds.alttp.Regions import mark_light_world_regions from ...Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from ...Shops import create_shops
from test.bases import TestBase
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestInvertedOWG(TestBase, LTTPTestBase): class TestInvertedOWG(TestBase, LTTPTestBase):

View File

@@ -1,5 +1,5 @@
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from test.bases import TestBase from ..bases import TestBase
base_items = 41 base_items = 41
extra_counts = (15, 15, 10, 5, 25) extra_counts = (15, 15, 10, 5, 25)

View File

@@ -1,11 +1,10 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions from ...InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from test.bases import TestBase from ...Options import GlitchesRequired
from worlds.alttp.Options import GlitchesRequired
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestMinor(TestBase, LTTPTestBase): class TestMinor(TestBase, LTTPTestBase):

View File

@@ -1,11 +1,10 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions from ...InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from test.bases import TestBase from ...Options import GlitchesRequired
from worlds.alttp.Options import GlitchesRequired
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestVanillaOWG(TestBase, LTTPTestBase): class TestVanillaOWG(TestBase, LTTPTestBase):

View File

@@ -1,5 +1,5 @@
from worlds.alttp.Shops import shop_table from ...Shops import shop_table
from test.bases import TestBase from ..bases import TestBase
class TestSram(TestBase): class TestSram(TestBase):

View File

@@ -1,10 +1,10 @@
from worlds.alttp.Dungeons import get_dungeon_item_pool from ...Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions from ...InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from ...ItemPool import difficulties
from worlds.alttp.Items import item_factory from ...Items import item_factory
from test.bases import TestBase from ...Options import GlitchesRequired
from worlds.alttp.Options import GlitchesRequired
from worlds.alttp.test import LTTPTestBase from ..bases import LTTPTestBase, TestBase
class TestVanilla(TestBase, LTTPTestBase): class TestVanilla(TestBase, LTTPTestBase):

View File

@@ -1,8 +1,8 @@
from test.bases import TestBase from unittest import TestCase
from ..data import Warp from ..data import Warp
class TestWarps(TestBase): class TestWarps(TestCase):
def test_warps_connect_ltr(self) -> None: def test_warps_connect_ltr(self) -> None:
# 2-way # 2-way
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0"))) self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0")))