MultiServer: Allow games with no locations, add checks to pure python implementation. (#1944)

* Server: allow games with no locations again

* Server: validate locations in pure python implementation

and rework tests

* Server: fix tests for py<3.11
This commit is contained in:
black-sliver 2023-07-05 10:35:03 +02:00 committed by GitHub
parent 50537a9161
commit 6fd16ecced
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 124 additions and 92 deletions

View File

@ -347,6 +347,18 @@ class Hint(typing.NamedTuple):
class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]): class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
def __init__(self, values: typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]):
super().__init__(values)
if not self:
raise ValueError(f"Rejecting game with 0 players")
if len(self) != max(self):
raise ValueError("Player IDs not continuous")
if len(self.get(0, {})):
raise ValueError("Invalid player id 0 for location")
def find_item(self, slots: typing.Set[int], seeked_item_id: int def find_item(self, slots: typing.Set[int], seeked_item_id: int
) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]: ) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]:
for finding_player, check_data in self.items(): for finding_player, check_data in self.items():

View File

@ -8,6 +8,7 @@ This is deliberately .pyx because using a non-compiled "pure python" may be slow
# pip install cython cymem # pip install cython cymem
import cython import cython
import warnings
from cpython cimport PyObject from cpython cimport PyObject
from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING from typing import Any, Dict, Iterable, Iterator, Generator, Sequence, Tuple, TypeVar, Union, Set, List, TYPE_CHECKING
from cymem.cymem cimport Pool from cymem.cymem cimport Pool
@ -107,13 +108,16 @@ cdef class LocationStore:
count += 1 count += 1
sender_count += 1 sender_count += 1
if not count: if not sender_count:
raise ValueError("No locations") raise ValueError(f"Rejecting game with 0 players")
if sender_count != max_sender: if sender_count != max_sender:
# we assume player 0 will never have locations # we assume player 0 will never have locations
raise ValueError("Player IDs not continuous") raise ValueError("Player IDs not continuous")
if not count:
warnings.warn("Game has no locations")
# allocate the arrays and invalidate index (0xff...) # allocate the arrays and invalidate index (0xff...)
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry)) self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry)) self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry))
@ -140,9 +144,9 @@ cdef class LocationStore:
self._proxies.append(None) # player 0 self._proxies.append(None) # player 0
assert self.sender_index[0].count == 0 assert self.sender_index[0].count == 0
for i in range(1, max_sender + 1): for i in range(1, max_sender + 1):
if self.sender_index[i].count == 0 and self.sender_index[i].start >= count: assert self.sender_index[i].count == 0 or (
self.sender_index[i].start = 0 # do not point outside valid entries self.sender_index[i].start < count and
assert self.sender_index[i].start < count self.sender_index[i].start + self.sender_index[i].count <= count)
key = i # allocate python integer key = i # allocate python integer
proxy = PlayerLocationProxy(self, i) proxy = PlayerLocationProxy(self, i)
self._keys.append(key) self._keys.append(key)

View File

@ -1,10 +1,13 @@
# Tests for _speedups.LocationStore and NetUtils._LocationStore # Tests for _speedups.LocationStore and NetUtils._LocationStore
import typing import typing
import unittest import unittest
import warnings
from NetUtils import LocationStore, _LocationStore from NetUtils import LocationStore, _LocationStore
State = typing.Dict[typing.Tuple[int, int], typing.Set[int]]
RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
sample_data = { sample_data: RawLocations = {
1: { 1: {
11: (21, 2, 7), 11: (21, 2, 7),
12: (22, 2, 0), 12: (22, 2, 0),
@ -23,28 +26,29 @@ sample_data = {
}, },
} }
empty_state = { empty_state: State = {
(0, slot): set() for slot in sample_data (0, slot): set() for slot in sample_data
} }
full_state = { full_state: State = {
(0, slot): set(locations) for (slot, locations) in sample_data.items() (0, slot): set(locations) for (slot, locations) in sample_data.items()
} }
one_state = { one_state: State = {
(0, 1): {12} (0, 1): {12}
} }
class Base: class Base:
class TestLocationStore(unittest.TestCase): class TestLocationStore(unittest.TestCase):
"""Test method calls on a loaded store."""
store: typing.Union[LocationStore, _LocationStore] store: typing.Union[LocationStore, _LocationStore]
def test_len(self): def test_len(self) -> None:
self.assertEqual(len(self.store), 4) self.assertEqual(len(self.store), 4)
self.assertEqual(len(self.store[1]), 3) self.assertEqual(len(self.store[1]), 3)
def test_key_error(self): def test_key_error(self) -> None:
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
_ = self.store[0] _ = self.store[0]
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
@ -54,25 +58,25 @@ class Base:
_ = locations[7] _ = locations[7]
_ = locations[11] # no Exception _ = locations[11] # no Exception
def test_getitem(self): def test_getitem(self) -> None:
self.assertEqual(self.store[1][11], (21, 2, 7)) self.assertEqual(self.store[1][11], (21, 2, 7))
self.assertEqual(self.store[1][13], (13, 1, 0)) self.assertEqual(self.store[1][13], (13, 1, 0))
self.assertEqual(self.store[2][22], (12, 1, 0)) self.assertEqual(self.store[2][22], (12, 1, 0))
self.assertEqual(self.store[4][9], (99, 3, 0)) self.assertEqual(self.store[4][9], (99, 3, 0))
def test_get(self): def test_get(self) -> None:
self.assertEqual(self.store.get(1, None), self.store[1]) self.assertEqual(self.store.get(1, None), self.store[1])
self.assertEqual(self.store.get(0, None), None) self.assertEqual(self.store.get(0, None), None)
self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11]) self.assertEqual(self.store[1].get(11, (None, None, None)), self.store[1][11])
self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None)) self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None))
def test_iter(self): def test_iter(self) -> None:
self.assertEqual(sorted(self.store), [1, 2, 3, 4]) self.assertEqual(sorted(self.store), [1, 2, 3, 4])
self.assertEqual(len(self.store), len(sample_data)) self.assertEqual(len(self.store), len(sample_data))
self.assertEqual(list(self.store[1]), [11, 12, 13]) self.assertEqual(list(self.store[1]), [11, 12, 13])
self.assertEqual(len(self.store[1]), len(sample_data[1])) self.assertEqual(len(self.store[1]), len(sample_data[1]))
def test_items(self): def test_items(self) -> None:
self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store)) self.assertEqual(sorted(p for p, _ in self.store.items()), sorted(self.store))
self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1])) self.assertEqual(sorted(p for p, _ in self.store[1].items()), sorted(self.store[1]))
self.assertEqual(sorted(self.store.items())[0][0], 1) self.assertEqual(sorted(self.store.items())[0][0], 1)
@ -80,7 +84,7 @@ class Base:
self.assertEqual(sorted(self.store[1].items())[0][0], 11) self.assertEqual(sorted(self.store[1].items())[0][0], 11)
self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11]) self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11])
def test_find_item(self): def test_find_item(self) -> None:
self.assertEqual(sorted(self.store.find_item(set(), 99)), []) self.assertEqual(sorted(self.store.find_item(set(), 99)), [])
self.assertEqual(sorted(self.store.find_item({3}, 1)), []) self.assertEqual(sorted(self.store.find_item({3}, 1)), [])
self.assertEqual(sorted(self.store.find_item({5}, 99)), []) self.assertEqual(sorted(self.store.find_item({5}, 99)), [])
@ -89,111 +93,73 @@ class Base:
self.assertEqual(sorted(self.store.find_item({3, 4}, 99)), self.assertEqual(sorted(self.store.find_item({3, 4}, 99)),
[(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)]) [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)])
def test_get_for_player(self): def test_get_for_player(self) -> None:
self.assertEqual(self.store.get_for_player(3), {4: {9}}) self.assertEqual(self.store.get_for_player(3), {4: {9}})
self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}}) self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
def get_checked(self): def get_checked(self) -> None:
self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13]) self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
self.assertEqual(self.store.get_checked(one_state, 0, 1), [12]) self.assertEqual(self.store.get_checked(one_state, 0, 1), [12])
self.assertEqual(self.store.get_checked(empty_state, 0, 1), []) self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
self.assertEqual(self.store.get_checked(full_state, 0, 3), [9]) self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
def get_missing(self): def get_missing(self) -> None:
self.assertEqual(self.store.get_missing(full_state, 0, 1), []) self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13]) self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13]) self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9]) self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
def get_remaining(self): def get_remaining(self) -> None:
self.assertEqual(self.store.get_remaining(full_state, 0, 1), []) self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21]) self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21])
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22]) self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22])
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99]) self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99])
class TestLocationStoreConstructor(unittest.TestCase):
"""Test constructors for a given store type."""
type: type
class TestPurePythonLocationStore(Base.TestLocationStore): def test_hole(self) -> None:
def setUp(self) -> None:
self.store = _LocationStore(sample_data)
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStore(Base.TestLocationStore):
def setUp(self) -> None:
self.store = LocationStore(sample_data)
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStoreConstructor(unittest.TestCase):
def test_float_key(self):
with self.assertRaises(Exception): with self.assertRaises(Exception):
LocationStore({ self.type({
1: {1: (1, 1, 1)},
1.1: {1: (1, 1, 1)},
3: {1: (1, 1, 1)}
})
def test_string_key(self):
with self.assertRaises(Exception):
LocationStore({
"1": {1: (1, 1, 1)},
})
def test_hole(self):
with self.assertRaises(Exception):
LocationStore({
1: {1: (1, 1, 1)}, 1: {1: (1, 1, 1)},
3: {1: (1, 1, 1)}, 3: {1: (1, 1, 1)},
}) })
def test_no_slot1(self): def test_no_slot1(self) -> None:
with self.assertRaises(Exception): with self.assertRaises(Exception):
LocationStore({ self.type({
2: {1: (1, 1, 1)}, 2: {1: (1, 1, 1)},
3: {1: (1, 1, 1)}, 3: {1: (1, 1, 1)},
}) })
def test_slot0(self): def test_slot0(self) -> None:
with self.assertRaises(Exception): with self.assertRaises(ValueError):
LocationStore({ self.type({
0: {1: (1, 1, 1)}, 0: {1: (1, 1, 1)},
1: {1: (1, 1, 1)}, 1: {1: (1, 1, 1)},
}) })
with self.assertRaises(Exception): with self.assertRaises(ValueError):
LocationStore({ self.type({
0: {1: (1, 1, 1)}, 0: {1: (1, 1, 1)},
2: {1: (1, 1, 1)}, 2: {1: (1, 1, 1)},
}) })
def test_high_player_number(self): def test_no_players(self) -> None:
with self.assertRaises(Exception): with self.assertRaises(Exception):
LocationStore({ _ = self.type({})
1 << 32: {1: (1, 1, 1)},
})
def test_no_players(self): def test_no_locations(self) -> None:
try: # either is fine: raise during init, or behave like {} with warnings.catch_warnings():
store = LocationStore({}) warnings.simplefilter("ignore")
self.assertEqual(len(store), 0) store = self.type({
with self.assertRaises(KeyError):
_ = store[1]
except ValueError:
pass
def test_no_locations(self):
try: # either is fine: raise during init, or behave like {1: {}}
store = LocationStore({
1: {}, 1: {},
}) })
self.assertEqual(len(store), 1) self.assertEqual(len(store), 1)
self.assertEqual(len(store[1]), 0) self.assertEqual(len(store[1]), 0)
except ValueError:
pass
def test_no_locations_for_1(self): def test_no_locations_for_1(self) -> None:
store = LocationStore({ store = self.type({
1: {}, 1: {},
2: {1: (1, 2, 3)}, 2: {1: (1, 2, 3)},
}) })
@ -201,8 +167,8 @@ class TestSpeedupsLocationStoreConstructor(unittest.TestCase):
self.assertEqual(len(store[1]), 0) self.assertEqual(len(store[1]), 0)
self.assertEqual(len(store[2]), 1) self.assertEqual(len(store[2]), 1)
def test_no_locations_for_last(self): def test_no_locations_for_last(self) -> None:
store = LocationStore({ store = self.type({
1: {1: (1, 2, 3)}, 1: {1: (1, 2, 3)},
2: {}, 2: {},
}) })
@ -210,8 +176,58 @@ class TestSpeedupsLocationStoreConstructor(unittest.TestCase):
self.assertEqual(len(store[1]), 1) self.assertEqual(len(store[1]), 1)
self.assertEqual(len(store[2]), 0) self.assertEqual(len(store[2]), 0)
def test_not_a_tuple(self):
class TestPurePythonLocationStore(Base.TestLocationStore):
"""Run base method tests for pure python implementation."""
def setUp(self) -> None:
self.store = _LocationStore(sample_data)
super().setUp()
class TestPurePythonLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests for the pure python implementation."""
def setUp(self) -> None:
self.type = _LocationStore
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStore(Base.TestLocationStore):
"""Run base method tests for cython implementation."""
def setUp(self) -> None:
self.store = LocationStore(sample_data)
super().setUp()
@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available")
class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor):
"""Run base constructor tests and tests the additional constraints for cython implementation."""
def setUp(self) -> None:
self.type = LocationStore
super().setUp()
def test_float_key(self) -> None:
with self.assertRaises(Exception): with self.assertRaises(Exception):
LocationStore({ self.type({
1: {1: (1, 1, 1)},
1.1: {1: (1, 1, 1)},
3: {1: (1, 1, 1)}
})
def test_string_key(self) -> None:
with self.assertRaises(Exception):
self.type({
"1": {1: (1, 1, 1)},
})
def test_high_player_number(self) -> None:
with self.assertRaises(Exception):
self.type({
1 << 32: {1: (1, 1, 1)},
})
def test_not_a_tuple(self) -> None:
with self.assertRaises(Exception):
self.type({
1: {1: None}, 1: {1: None},
}) })