mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-05 02:38:14 -07:00
Merge branch 'ArchipelagoMW:main' into Satisfactory
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(ap-cpp-tests)
|
||||
|
||||
enable_testing()
|
||||
@@ -7,8 +7,8 @@ find_package(GTest REQUIRED)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||
add_definitions("/source-charset:utf-8")
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
|
||||
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
|
||||
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# enable static analysis for gcc
|
||||
add_compile_options(-fanalyzer -Werror)
|
||||
|
||||
@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets shuffles targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -86,8 +88,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets does not shuffle targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -99,6 +103,30 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
||||
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
||||
|
||||
def test_selective_dead_ends(self):
|
||||
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region
|
||||
and ex.name != "region20_right" and ex.name != "region21_left"])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region and
|
||||
entrance.name != "region20_right" and entrance.name != "region21_left"]
|
||||
for entrance in er_targets:
|
||||
lookup.add(entrance)
|
||||
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
|
||||
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
|
||||
# the top entrance from region 15 should be considered a dead-end
|
||||
dead_end_region = multiworld.get_region("region20", 1)
|
||||
for dead_end in dead_end_region.entrances:
|
||||
if dead_end.name == "region20_top":
|
||||
break
|
||||
# there should be only this one dead-end
|
||||
self.assertTrue(dead_end in lookup.dead_ends)
|
||||
self.assertEqual(len(lookup.dead_ends), 1)
|
||||
|
||||
class TestBakeTargetGroupLookup(unittest.TestCase):
|
||||
def test_lookup_generation(self):
|
||||
@@ -148,7 +176,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e)
|
||||
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -158,10 +186,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
||||
|
||||
def test_disconnect_default_1way_no_vanilla_target_raises(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
r2 = Region("r2", 1, multiworld)
|
||||
e = r1.create_exit("e")
|
||||
e.randomization_type = EntranceType.ONE_WAY
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
disconnect_entrance_for_randomization(e)
|
||||
|
||||
def test_disconnect_uses_alternate_group(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
@@ -171,7 +211,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e, 2)
|
||||
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -181,7 +221,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
||||
|
||||
@@ -218,7 +258,7 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
self.assertEqual(80, len(result.pairings))
|
||||
self.assertEqual(80, len(result.placements))
|
||||
|
||||
def test_coupling(self):
|
||||
def test_coupled(self):
|
||||
"""tests that in coupled mode, all 2 way transitions have an inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
@@ -236,6 +276,36 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
||||
self.assertEqual(len(result.placements), seen_placement_count)
|
||||
|
||||
def test_uncoupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_coupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_uncoupled(self):
|
||||
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
@@ -311,6 +381,37 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
self.assertEqual([], [exit_ for region in multiworld.get_regions()
|
||||
for exit_ in region.exits if not exit_.connected_region])
|
||||
|
||||
def test_minimal_entrance_rando_with_collect_override(self):
|
||||
"""
|
||||
tests that entrance randomization can complete with minimal accessibility and unreachable exits
|
||||
when the world defines a collect override that add extra values to prog_items
|
||||
"""
|
||||
multiworld = generate_test_multiworld()
|
||||
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
|
||||
generate_disconnected_region_grid(multiworld, 5, 1)
|
||||
prog_items = generate_items(10, 1, True)
|
||||
multiworld.itempool += prog_items
|
||||
filler_items = generate_items(15, 1, False)
|
||||
multiworld.itempool += filler_items
|
||||
e = multiworld.get_entrance("region1_right", 1)
|
||||
set_rule(e, lambda state: False)
|
||||
|
||||
old_collect = multiworld.worlds[1].collect
|
||||
|
||||
def new_collect(state, item):
|
||||
old_collect(state, item)
|
||||
state.prog_items[item.player]["counter"] += 300
|
||||
|
||||
multiworld.worlds[1].collect = new_collect
|
||||
|
||||
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
|
||||
|
||||
self.assertEqual([], [entrance for region in multiworld.get_regions()
|
||||
for entrance in region.entrances if not entrance.parent_region])
|
||||
self.assertEqual([], [exit_ for region in multiworld.get_regions()
|
||||
for exit_ in region.exits if not exit_.connected_region])
|
||||
|
||||
def test_restrictive_region_requirement_does_not_fail(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 2, 1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
@@ -8,12 +9,31 @@ class TestBase(unittest.TestCase):
|
||||
def test_create_item(self):
|
||||
"""Test that a world can successfully create all items in its datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
|
||||
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
|
||||
proxy_world = multiworld.worlds[1]
|
||||
for item_name in world_type.item_name_to_id:
|
||||
test_state = CollectionState(multiworld)
|
||||
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
|
||||
item = proxy_world.create_item(item_name)
|
||||
|
||||
with self.subTest("Item Name", item_name=item_name, game_name=game_name):
|
||||
self.assertEqual(item.name, item_name)
|
||||
|
||||
if item.advancement:
|
||||
with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
|
||||
test_state.collect(item, True)
|
||||
|
||||
with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
|
||||
test_state.remove(item)
|
||||
|
||||
self.assertEqual(test_state.prog_items, multiworld.state.prog_items,
|
||||
"Item Collect -> Remove should restore empty state.")
|
||||
else:
|
||||
with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
|
||||
# Non-Advancement should not modify state.
|
||||
test_state.collect(item)
|
||||
self.assertEqual(test_state.prog_items, multiworld.state.prog_items)
|
||||
|
||||
def test_item_name_group_has_valid_item(self):
|
||||
"""Test that all item name groups contain valid items. """
|
||||
# This cannot test for Event names that you may have declared for logic, only sendable Items.
|
||||
|
||||
14
test/general/test_packages.py
Normal file
14
test/general/test_packages.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestPackages(unittest.TestCase):
|
||||
def test_packages_have_init(self):
|
||||
"""Test that all world folders containing .py files also have a __init__.py file,
|
||||
to indicate full package rather than namespace package."""
|
||||
import Utils
|
||||
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for dirpath, dirnames, filenames in os.walk(worlds_path):
|
||||
with self.subTest(directory=dirpath):
|
||||
self.assertEqual("__init__.py" in filenames, any(file.endswith(".py") for file in filenames))
|
||||
11
test/general/test_patches.py
Normal file
11
test/general/test_patches.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.Files import AutoPatchRegister
|
||||
|
||||
|
||||
class TestPatches(unittest.TestCase):
|
||||
def test_patch_name_matches_game(self) -> None:
|
||||
for game_name in AutoPatchRegister.patch_types:
|
||||
with self.subTest(game=game_name):
|
||||
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
|
||||
f"Patch '{game_name}' does not match the name of any world.")
|
||||
19
test/general/test_requirements.py
Normal file
19
test/general/test_requirements.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_requirements_file_ends_on_newline(self):
|
||||
"""Test that all requirements files end on a newline"""
|
||||
import Utils
|
||||
requirements_files = [Utils.local_path("requirements.txt"),
|
||||
Utils.local_path("WebHostLib", "requirements.txt")]
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for entry in os.listdir(worlds_path):
|
||||
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
|
||||
if os.path.isfile(requirements_path):
|
||||
requirements_files.append(requirements_path)
|
||||
for requirements_file in requirements_files:
|
||||
with self.subTest(path=requirements_file):
|
||||
with open(requirements_file) as f:
|
||||
self.assertEqual(f.read()[-1], "\n")
|
||||
29
test/general/test_state.py
Normal file
29
test/general/test_state.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import unittest
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
gen_steps = (
|
||||
"generate_early",
|
||||
"create_regions",
|
||||
)
|
||||
|
||||
test_steps = (
|
||||
"create_items",
|
||||
"set_rules",
|
||||
"connect_entrances",
|
||||
"generate_basic",
|
||||
"pre_fill",
|
||||
)
|
||||
|
||||
def test_all_state_is_available(self):
|
||||
"""Ensure all_state can be created at certain steps."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
|
||||
for step in self.test_steps:
|
||||
with self.subTest("Step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertTrue(multiworld.get_all_state(False, True))
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from typing import List, Tuple
|
||||
from typing import ClassVar, List, Tuple
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||
from ..general import gen_steps, setup_multiworld
|
||||
from ..param import classvar_matrix
|
||||
|
||||
|
||||
class MultiworldTestBase(TestCase):
|
||||
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
|
||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
game: ClassVar[str]
|
||||
|
||||
def test_two_player_single_game_fills(self) -> None:
|
||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||
for world_type in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
world_type = AutoWorldRegister.world_types[self.game]
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
46
test/param.py
Normal file
46
test/param.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
|
||||
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
|
||||
"""
|
||||
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
|
||||
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
|
||||
than subtests.
|
||||
|
||||
The kwargs will be set as ClassVars in the newly created classes. Use as ::
|
||||
|
||||
@classvar_matrix(var_name=[value1, value2])
|
||||
class MyTestCase(unittest.TestCase):
|
||||
var_name: typing.ClassVar[...]
|
||||
|
||||
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
|
||||
:return: A decorator to be applied to a class.
|
||||
"""
|
||||
keys: tuple[str]
|
||||
values: Iterable[Iterable[Any]]
|
||||
keys, values = zip(*kwargs.items())
|
||||
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
|
||||
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
|
||||
|
||||
def decorator(cls: type) -> None:
|
||||
mod = sys.modules[cls.__module__]
|
||||
|
||||
for permutation in permutations_dicts:
|
||||
|
||||
class Unrolled(cls): # type: ignore
|
||||
pass
|
||||
|
||||
for k, v in permutation.items():
|
||||
setattr(Unrolled, k, v)
|
||||
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
|
||||
params = f"{{{params}}}"
|
||||
|
||||
Unrolled.__module__ = cls.__module__
|
||||
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
|
||||
setattr(mod, f"{cls.__name__}{params}", Unrolled)
|
||||
|
||||
return None
|
||||
|
||||
return decorator
|
||||
Reference in New Issue
Block a user