Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion test/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None:
random.seed(self.multiworld.seed)
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
world_type = AutoWorld.AutoWorldRegister.testable_worlds[self.game].world_type
for name, option in world_type.options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(self.options.get(name, option.default))
})
Expand Down
8 changes: 4 additions & 4 deletions test/general/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@
)


def setup_solo_multiworld(
world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None
) -> MultiWorld:
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None,
options: dict[str, Any] | None = None) -> MultiWorld:
"""
Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.

:param world_type: Type of the world to generate a multiworld for
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
steps through pre_fill
:param seed: The seed to be used when creating this multiworld
:param options: Options to set on the world.
:return: The generated multiworld
"""
return setup_multiworld(world_type, steps, seed)
return setup_multiworld(world_type, steps, seed, options)


def setup_multiworld(worlds: list[type[World]] | type[World], steps: tuple[str, ...] = gen_steps,
Expand Down
64 changes: 34 additions & 30 deletions test/general/test_entrances.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,27 @@ def get_entrance_name_to_source_and_target_dict(world: World):
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():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
for game_name, testable_world in AutoWorldRegister.testable_worlds.items():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how often this pattern is used, it seems like it would be desirable to make it easier to reuse this as it's getting very bulky and inconvenient to use. It'll also make future refactoring easier.

One option that comes to mind but may be neither optimal nor trivial is to make an decorator that automatically implements this pattern, allowing each test to be implemented as a function of world/options. The iteration and subtests would then be handled opaquely under the hood for a majority of tests

world_type = testable_world.world_type
for options_name, options in testable_world.testable_options_by_name.items():
with self.subTest("Game", game_name=game_name, options=options_name):
multiworld = setup_solo_multiworld(world_type, gen_steps, options=options)

original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])

self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)
self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)

for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])

self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)
self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)

def test_all_state_before_connect_entrances(self):
"""Before connect_entrances, Entrance objects may be unconnected.
Expand All @@ -42,23 +44,25 @@ def test_all_state_before_connect_entrances(self):

gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")

for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
for game_name, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
for options_name, options in testable_world.testable_options_by_name.items():
with self.subTest("Game", game_name=game_name, options=options_name):
multiworld = setup_solo_multiworld(world_type, (), options=options)

original_get_all_state = multiworld.get_all_state
original_get_all_state = multiworld.get_all_state

def patched_get_all_state(use_cache: bool | None = None, allow_partial_entrances: bool = False,
**kwargs):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
def patched_get_all_state(use_cache: bool | None = None, allow_partial_entrances: bool = False,
**kwargs):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))

return original_get_all_state(use_cache, allow_partial_entrances, **kwargs)
return original_get_all_state(use_cache, allow_partial_entrances, **kwargs)

multiworld.get_all_state = patched_get_all_state
multiworld.get_all_state = patched_get_all_state

for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
6 changes: 4 additions & 2 deletions test/general/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
if not world_type.item_id_to_name:
continue # ignore worlds without items
with self.subTest(game=game_name):
Expand All @@ -19,7 +20,8 @@ def test_location_name_groups_not_empty(self) -> None:
"""
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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
if not world_type.location_id_to_name:
continue # ignore worlds without locations
with self.subTest(game=game_name):
Expand Down
74 changes: 41 additions & 33 deletions test/general/test_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,24 @@
class TestIDs(unittest.TestCase):
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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
with self.subTest(game=gamename):
if gamename == "Archipelago":
for item_id in world_type.item_id_to_name:
Expand All @@ -34,7 +37,8 @@ def test_reserved_items(self):

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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
with self.subTest(game=gamename):
if gamename == "Archipelago":
for location_id in world_type.location_id_to_name:
Expand All @@ -45,7 +49,8 @@ def test_reserved_locations(self):

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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
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)
Expand All @@ -64,7 +69,8 @@ def test_duplicate_item_ids(self):

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, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
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)
Expand All @@ -83,32 +89,34 @@ def test_duplicate_location_ids(self):

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():
with self.subTest(game=gamename):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
f"item_name_group names should be strings: {item_group}")
for item_name in item_names:
for gamename, testable_world in AutoWorldRegister.testable_worlds.items():
world_type = testable_world.world_type
for options_name, options in testable_world.testable_options_by_name.items():
with self.subTest(game=gamename, options=options_name):
multiworld = setup_solo_multiworld(world_type, options=options)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
f"item_name_group names should be strings: {item_group}")
for item_name in item_names:
self.assertIsInstance(item_name, str,
f"{item_name}, in group {item_group} is not a string")
for loc_group, loc_names in datapackage["location_name_groups"].items():
self.assertIsInstance(loc_group, str,
f"location_name_group names should be strings: {loc_group}")
for loc_name in loc_names:
self.assertIsInstance(loc_name, str,
f"{loc_name}, in group {loc_group} is not a string")
for item_name, item_id in datapackage["item_name_to_id"].items():
self.assertIsInstance(item_name, str,
f"{item_name}, in group {item_group} is not a string")
for loc_group, loc_names in datapackage["location_name_groups"].items():
self.assertIsInstance(loc_group, str,
f"location_name_group names should be strings: {loc_group}")
for loc_name in loc_names:
f"{item_name} is not a valid item name for item_name_to_id")
self.assertIsInstance(item_id, int,
f"{item_id} for {item_name} should be an int")
for loc_name, loc_id in datapackage["location_name_to_id"].items():
self.assertIsInstance(loc_name, str,
f"{loc_name}, in group {loc_group} is not a string")
for item_name, item_id in datapackage["item_name_to_id"].items():
self.assertIsInstance(item_name, str,
f"{item_name} is not a valid item name for item_name_to_id")
self.assertIsInstance(item_id, int,
f"{item_id} for {item_name} should be an int")
for loc_name, loc_id in datapackage["location_name_to_id"].items():
self.assertIsInstance(loc_name, str,
f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int")
self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"])
f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int")
self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"])
Loading
Loading