mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-09-29 13:34:32 -07:00
* MultiServer, customserver: move data package handling Create a new class that handles conversion worlds+embedded -> context data. Create a derived class that uses static_server_data+pony instead. There is also a not very efficient feature to deduplicate strings (may need perf testing). By moving code around, we can simplify a lot of the world loading. Where code lines were touched, some typing and some reformatting was added. The back compat for GetDataPackage without games was finally dropped. This was done as a cleanup because the refactoring touched those lines anyway. Also reworked the per-context dicts and the RoomInfo to hopefully be more efficient by ignoring unused games. (Generating the list of used games was required for the new code anyway.) Side effect of the MultiServer cache: we now load worlds lazily (but still all at once) and don't modify the games package in place. If needed we create copies. This almost gets us to the point where MultiServer doesn't need worlds - it still needs them for the forbidden items. There is a bonus optimization that deduplicates strings in name_groups that may have bad performance and may need some perf testing if we run into issues. * MultiServer: fix string deduplication in data package cache * MultiServer, customserver: minor formatting fixes * MultiServer: fix data package cache for missing checksum case * MultiServer: improve string deduplication in games package cache * customserver: handle missing checksum in datapackage cache * customserver: typing cleanup in games package cache * Test, MultiServer, customser: add tests for games package cache * Test, MultiServer: reorder imports Hopefully this fixes the random test failures with pytest-xdist * customserver: make WebHost import lazy in games package cache and fix test * customserver: games package cache: fix py3.11 compat * MultiServer, customserver: cache: typing improvements * customserver: cache: reformat * Test: move customserver tests to not interfere with webhost * MultiServer, customserver: cache: rename module * Multiserver: better error message for missing Datapackage * customserver: cache: invert if for readability * customserver: optimize non_hintable_names * Use an empty frozenset singleton * Only include non-empty sets in static_server_data * Only prefill dict with played games This should save ~2-4KB per room and slightly speedup lookup. --------- Co-authored-by: Fabian Dill <[email protected]>
133 lines
5.6 KiB
Python
133 lines
5.6 KiB
Python
import typing as t
|
|
from copy import deepcopy
|
|
from unittest import TestCase
|
|
|
|
from typing_extensions import override
|
|
|
|
import NetUtils
|
|
from NetUtils import GamesPackage
|
|
from apmw.multiserver.gamespackagecache import GamesPackageCache
|
|
|
|
|
|
class GamesPackageCacheTest(TestCase):
|
|
cache: GamesPackageCache
|
|
any_game: t.ClassVar[str] = "APQuest"
|
|
example_games_package: GamesPackage = {
|
|
"item_name_to_id": {"Item 1": 1},
|
|
"item_name_groups": {"Everything": ["Item 1"]},
|
|
"location_name_to_id": {"Location 1": 1},
|
|
"location_name_groups": {"Everywhere": ["Location 1"]},
|
|
"checksum": "1234",
|
|
}
|
|
|
|
@override
|
|
def setUp(self) -> None:
|
|
self.cache = GamesPackageCache()
|
|
|
|
def test_get_static_is_same(self) -> None:
|
|
"""Tests that get_static returns the same objects twice"""
|
|
reduced_games_package1, item_name_groups1, location_name_groups1 = self.cache.get_static(self.any_game)
|
|
reduced_games_package2, item_name_groups2, location_name_groups2 = self.cache.get_static(self.any_game)
|
|
self.assertIs(reduced_games_package1, reduced_games_package2)
|
|
self.assertIs(item_name_groups1, item_name_groups2)
|
|
self.assertIs(location_name_groups1, location_name_groups2)
|
|
|
|
def test_get_static_data_format(self) -> None:
|
|
"""Tests that get_static returns data in the correct format"""
|
|
reduced_games_package, item_name_groups, location_name_groups = self.cache.get_static(self.any_game)
|
|
self.assertTrue(reduced_games_package["checksum"])
|
|
self.assertTrue(reduced_games_package["item_name_to_id"])
|
|
self.assertTrue(reduced_games_package["location_name_to_id"])
|
|
self.assertNotIn("item_name_groups", reduced_games_package)
|
|
self.assertNotIn("location_name_groups", reduced_games_package)
|
|
self.assertTrue(item_name_groups["Everything"])
|
|
self.assertTrue(location_name_groups["Everywhere"])
|
|
|
|
def test_get_static_is_serializable(self) -> None:
|
|
"""Tests that get_static returns data that can be serialized"""
|
|
NetUtils.encode(self.cache.get_static(self.any_game))
|
|
|
|
def test_get_static_missing_raises(self) -> None:
|
|
"""Tests that get_static raises KeyError if the world is missing"""
|
|
with self.assertRaises(KeyError):
|
|
_ = self.cache.get_static("Does not exist")
|
|
|
|
def test_eviction(self) -> None:
|
|
"""Tests that unused items get evicted from cache"""
|
|
game_name = "Test"
|
|
before_add = len(self.cache._reduced_games_packages)
|
|
data = self.cache.get(game_name, self.example_games_package)
|
|
self.assertTrue(data)
|
|
self.assertEqual(before_add + 1, len(self.cache._reduced_games_packages))
|
|
|
|
del data
|
|
if len(self.cache._reduced_games_packages) != before_add: # gc.collect() may not even be required
|
|
import gc
|
|
|
|
gc.collect()
|
|
|
|
self.assertEqual(before_add, len(self.cache._reduced_games_packages))
|
|
|
|
def test_get_required_field(self) -> None:
|
|
"""Tests that missing required field raises a KeyError"""
|
|
for field in ("item_name_to_id", "location_name_to_id", "item_name_groups"):
|
|
with self.subTest(field=field):
|
|
games_package = deepcopy(self.example_games_package)
|
|
del games_package[field] # type: ignore
|
|
with self.assertRaises(KeyError):
|
|
_ = self.cache.get(self.any_game, games_package)
|
|
|
|
def test_get_optional_properties(self) -> None:
|
|
"""Tests that missing optional field works"""
|
|
for field in ("checksum", "location_name_groups"):
|
|
with self.subTest(field=field):
|
|
games_package = deepcopy(self.example_games_package)
|
|
del games_package[field] # type: ignore
|
|
_, item_name_groups, location_name_groups = self.cache.get(self.any_game, games_package)
|
|
self.assertTrue(item_name_groups)
|
|
self.assertEqual(field != "location_name_groups", bool(location_name_groups))
|
|
|
|
def test_item_name_deduplication(self) -> None:
|
|
n = 1
|
|
s1 = f"Item {n}"
|
|
s2 = f"Item {n}"
|
|
# check if the deduplication is actually gonna do anything
|
|
self.assertIsNot(s1, s2)
|
|
self.assertEqual(s1, s2)
|
|
# do the thing
|
|
game_name = "Test"
|
|
games_package: GamesPackage = {
|
|
"item_name_to_id": {s1: n},
|
|
"item_name_groups": {"Everything": [s2]},
|
|
"location_name_to_id": {},
|
|
"location_name_groups": {},
|
|
"checksum": "1234",
|
|
}
|
|
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
|
|
self.assertIs(
|
|
next(iter(reduced_games_package["item_name_to_id"].keys())),
|
|
item_name_groups["Everything"][0],
|
|
)
|
|
|
|
def test_location_name_deduplication(self) -> None:
|
|
n = 1
|
|
s1 = f"Location {n}"
|
|
s2 = f"Location {n}"
|
|
# check if the deduplication is actually gonna do anything
|
|
self.assertIsNot(s1, s2)
|
|
self.assertEqual(s1, s2)
|
|
# do the thing
|
|
game_name = "Test"
|
|
games_package: GamesPackage = {
|
|
"item_name_to_id": {},
|
|
"item_name_groups": {},
|
|
"location_name_to_id": {s1: n},
|
|
"location_name_groups": {"Everywhere": [s2]},
|
|
"checksum": "1234",
|
|
}
|
|
reduced_games_package, item_name_groups, location_name_groups = self.cache.get(game_name, games_package)
|
|
self.assertIs(
|
|
next(iter(reduced_games_package["location_name_to_id"].keys())),
|
|
location_name_groups["Everywhere"][0],
|
|
)
|