diff --git a/docs/tests.md b/docs/tests.md index 78cedbc5..0740072d 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -82,10 +82,10 @@ overridden. For more information on what methods are available to your class, ch #### Alternatives to WorldTestBase -Unit tests can also be created using [TestBase](/test/bases.py#L16) or -[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These -may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for -testing portions of your code that can be tested without relying on a multiworld to be created first. +Unit tests can also be created using +[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) directly. These may be useful +for generating a multiworld under very specific constraints without using the generic world setup, or for testing +portions of your code that can be tested without relying on a multiworld to be created first. #### 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 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 - or setting `WorldTestBase.run_default_tests` to False. + extra CPU time. Consider using `unittest.TestCase` directly or setting `WorldTestBase.run_default_tests` to False. #### Performance Considerations diff --git a/test/bases.py b/test/bases.py index c9610c86..dd93ca64 100644 --- a/test/bases.py +++ b/test/bases.py @@ -9,98 +9,7 @@ from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import World, call_all -from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, 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) +from BaseClasses import Location, MultiWorld, CollectionState, Item class WorldTestBase(unittest.TestCase): diff --git a/worlds/alttp/test/__init__.py b/worlds/alttp/test/__init__.py index 031d5086..e69de29b 100644 --- a/worlds/alttp/test/__init__.py +++ b/worlds/alttp/test/__init__.py @@ -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 diff --git a/worlds/alttp/test/bases.py b/worlds/alttp/test/bases.py new file mode 100644 index 00000000..c3b4f47a --- /dev/null +++ b/worlds/alttp/test/bases.py @@ -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 diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index c06955a1..dd622c4f 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -5,7 +5,7 @@ from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.test import LTTPTestBase +from worlds.alttp.test.bases import LTTPTestBase class TestDungeon(LTTPTestBase): diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 3c86b6ba..8dc02de7 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -1,13 +1,12 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.EntranceShuffle import link_inverted_entrances -from worlds.alttp.InvertedRegions import create_inverted_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from worlds.alttp.Regions import mark_light_world_regions -from worlds.alttp.Shops import create_shops -from test.bases import TestBase +from ...Dungeons import get_dungeon_item_pool +from ...EntranceShuffle import link_inverted_entrances +from ...InvertedRegions import create_inverted_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Regions import mark_light_world_regions +from ...Shops import create_shops -from worlds.alttp.test import LTTPTestBase +from ..bases import LTTPTestBase, TestBase class TestInverted(TestBase, LTTPTestBase): diff --git a/worlds/alttp/test/inverted/TestInvertedBombRules.py b/worlds/alttp/test/inverted/TestInvertedBombRules.py index ab73d911..5ad52709 100644 --- a/worlds/alttp/test/inverted/TestInvertedBombRules.py +++ b/worlds/alttp/test/inverted/TestInvertedBombRules.py @@ -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.ItemPool import difficulties 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): diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index 972b617a..958cd3e7 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -1,14 +1,13 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.EntranceShuffle import link_inverted_entrances -from worlds.alttp.InvertedRegions import create_inverted_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from worlds.alttp.Options import GlitchesRequired -from worlds.alttp.Regions import mark_light_world_regions -from worlds.alttp.Shops import create_shops -from test.bases import TestBase +from ...Dungeons import get_dungeon_item_pool +from ...EntranceShuffle import link_inverted_entrances +from ...InvertedRegions import create_inverted_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Options import GlitchesRequired +from ...Regions import mark_light_world_regions +from ...Shops import create_shops -from worlds.alttp.test import LTTPTestBase +from ..bases import LTTPTestBase, TestBase class TestInvertedMinor(TestBase, LTTPTestBase): diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index 4be51f62..8a6b570d 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -1,14 +1,13 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.EntranceShuffle import link_inverted_entrances -from worlds.alttp.InvertedRegions import create_inverted_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from worlds.alttp.Options import GlitchesRequired -from worlds.alttp.Regions import mark_light_world_regions -from worlds.alttp.Shops import create_shops -from test.bases import TestBase +from ...Dungeons import get_dungeon_item_pool +from ...EntranceShuffle import link_inverted_entrances +from ...InvertedRegions import create_inverted_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Options import GlitchesRequired +from ...Regions import mark_light_world_regions +from ...Shops import create_shops -from worlds.alttp.test import LTTPTestBase +from ..bases import LTTPTestBase, TestBase class TestInvertedOWG(TestBase, LTTPTestBase): diff --git a/worlds/alttp/test/items/TestDifficulty.py b/worlds/alttp/test/items/TestDifficulty.py index 69dd8a4d..ff4deb85 100644 --- a/worlds/alttp/test/items/TestDifficulty.py +++ b/worlds/alttp/test/items/TestDifficulty.py @@ -1,5 +1,5 @@ -from worlds.alttp.ItemPool import difficulties -from test.bases import TestBase +from ...ItemPool import difficulties +from ..bases import TestBase base_items = 41 extra_counts = (15, 15, 10, 5, 25) diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index d5ffe8ca..5ffb1ca8 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -1,11 +1,10 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.InvertedRegions import mark_dark_world_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from test.bases import TestBase -from worlds.alttp.Options import GlitchesRequired +from ...Dungeons import get_dungeon_item_pool +from ...InvertedRegions import mark_dark_world_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Options import GlitchesRequired -from worlds.alttp.test import LTTPTestBase +from ..bases import LTTPTestBase, TestBase class TestMinor(TestBase, LTTPTestBase): diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index 6b6db145..6c246865 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -1,11 +1,10 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.InvertedRegions import mark_dark_world_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from test.bases import TestBase -from worlds.alttp.Options import GlitchesRequired +from ...Dungeons import get_dungeon_item_pool +from ...InvertedRegions import mark_dark_world_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Options import GlitchesRequired -from worlds.alttp.test import LTTPTestBase +from ..bases import LTTPTestBase, TestBase class TestVanillaOWG(TestBase, LTTPTestBase): diff --git a/worlds/alttp/test/shops/TestSram.py b/worlds/alttp/test/shops/TestSram.py index 74a41a62..a7dfd37c 100644 --- a/worlds/alttp/test/shops/TestSram.py +++ b/worlds/alttp/test/shops/TestSram.py @@ -1,5 +1,5 @@ -from worlds.alttp.Shops import shop_table -from test.bases import TestBase +from ...Shops import shop_table +from ..bases import TestBase class TestSram(TestBase): diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index 031aec1f..2ddcdadc 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -1,10 +1,10 @@ -from worlds.alttp.Dungeons import get_dungeon_item_pool -from worlds.alttp.InvertedRegions import mark_dark_world_regions -from worlds.alttp.ItemPool import difficulties -from worlds.alttp.Items import item_factory -from test.bases import TestBase -from worlds.alttp.Options import GlitchesRequired -from worlds.alttp.test import LTTPTestBase +from ...Dungeons import get_dungeon_item_pool +from ...InvertedRegions import mark_dark_world_regions +from ...ItemPool import difficulties +from ...Items import item_factory +from ...Options import GlitchesRequired + +from ..bases import LTTPTestBase, TestBase class TestVanilla(TestBase, LTTPTestBase): diff --git a/worlds/pokemon_emerald/test/test_warps.py b/worlds/pokemon_emerald/test/test_warps.py index d1b5b01d..f210ff6b 100644 --- a/worlds/pokemon_emerald/test/test_warps.py +++ b/worlds/pokemon_emerald/test/test_warps.py @@ -1,8 +1,8 @@ -from test.bases import TestBase +from unittest import TestCase from ..data import Warp -class TestWarps(TestBase): +class TestWarps(TestCase): def test_warps_connect_ltr(self) -> None: # 2-way self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0")))