From f3bdf0c5ed3b6099e33cc17a095b27ee19712d3a Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 26 Feb 2023 18:24:54 -0600 Subject: [PATCH] Tests: test all state and empty state on world test bases (#1476) * Tests: test all state and empty state on world test bases * actually add the test methods to the dict * only test if the world test base has non default options * remove temp logging * ditch the meta class and document methods * Tests: WorldTestBase comment and docstring cleanup * skip default tests if setUp or world_setup are modified and use a property * negation hurts my head * docstring * use a better name for the property --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/TestBase.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/TestBase.py b/test/TestBase.py index eea8e81a..fb8031a9 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -112,6 +112,8 @@ class WorldTestBase(unittest.TestCase): self.world_setup() def world_setup(self, seed: typing.Optional[int] = None) -> None: + if type(self) is WorldTestBase: + return # setUp gets called for tests defined in the base class. We skip world_setup here. if not hasattr(self, "game"): raise NotImplementedError("didn't define game name") self.multiworld = MultiWorld(1) @@ -128,7 +130,9 @@ class WorldTestBase(unittest.TestCase): for step in gen_steps: call_all(self.multiworld, step) + # methods that can be called within tests def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None: + """Collects all pre-placed items and items in the multiworld itempool except those provided""" if isinstance(item_names, str): item_names = (item_names,) for item in self.multiworld.get_items(): @@ -136,12 +140,14 @@ class WorldTestBase(unittest.TestCase): self.multiworld.state.collect(item) def get_item_by_name(self, item_name: str) -> Item: + """Returns the first item found in placed items, or in the itempool with the matching name""" for item in self.multiworld.get_items(): if item.name == item_name: return item raise ValueError("No such item") def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]: + """Returns actual items from the itempool that match the provided name(s)""" if isinstance(item_names, str): item_names = (item_names,) return [item for item in self.multiworld.itempool if item.name in item_names] @@ -153,12 +159,14 @@ class WorldTestBase(unittest.TestCase): return items def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Collects the provided item(s) into state""" if isinstance(items, Item): items = (items,) for item in items: self.multiworld.state.collect(item) def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None: + """Removes the provided item(s) from state""" if isinstance(items, Item): items = (items,) for item in items: @@ -167,17 +175,22 @@ class WorldTestBase(unittest.TestCase): self.multiworld.state.remove(item) def can_reach_location(self, location: str) -> bool: + """Determines if the current state can reach the provide location name""" return self.multiworld.state.can_reach(location, "Location", 1) def can_reach_entrance(self, entrance: str) -> bool: + """Determines if the current state can reach the provided entrance name""" return self.multiworld.state.can_reach(entrance, "Entrance", 1) def count(self, item_name: str) -> int: + """Returns the amount of an item currently in state""" return self.multiworld.state.count(item_name, 1) def assertAccessDependency(self, locations: typing.List[str], possible_items: typing.Iterable[typing.Iterable[str]]) -> None: + """Asserts that the provided locations can't be reached without the listed items but can be reached with any + one of the provided combinations""" all_items = [item_name for item_names in possible_items for item_name in item_names] self.collect_all_but(all_items) @@ -190,4 +203,36 @@ class WorldTestBase(unittest.TestCase): self.remove(items) def assertBeatable(self, beatable: bool): + """Asserts that the game can be beaten with the current state""" self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable) + + # following tests are automatically run + @property + def skip_default_tests(self) -> bool: + """Not possible or identical to the base test that's always being run already""" + constructed = hasattr(self, "game") and hasattr(self, "multiworld") + return not constructed or (not self.options + and self.setUp is WorldTestBase.setUp + and self.world_setup is WorldTestBase.world_setup) + + def testAllStateCanReachEverything(self): + """Ensure all state can reach everything with the defined options""" + if self.skip_default_tests: + return + with self.subTest("Game", game=self.game): + excluded = self.multiworld.exclude_locations[1].value + state = self.multiworld.get_all_state(False) + for location in self.multiworld.get_locations(): + if location.name not in excluded: + with self.subTest("Location should be reached", location=location): + self.assertTrue(location.can_reach(state), f"{location.name} unreachable") + + def testEmptyStateCanReachSomething(self): + """Ensure empty state can reach at least one location with the defined options""" + if self.skip_default_tests: + return + with self.subTest("Game", game=self.game): + state = CollectionState(self.multiworld) + locations = self.multiworld.get_reachable_locations(state, 1) + self.assertGreater(len(locations), 0, + "Need to be able to reach at least one location to get started.")