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>
This commit is contained in:
alwaysintreble 2023-02-26 18:24:54 -06:00 committed by GitHub
parent af7d0dbf37
commit f3bdf0c5ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 45 additions and 0 deletions

View File

@ -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.")