From bc78cbb46e2144b7a4b4d5b58d1355f8f8cc7e10 Mon Sep 17 00:00:00 2001 From: Diego Baldassar Date: Fri, 20 Dec 2024 18:36:30 +0100 Subject: [PATCH 1/2] Add tests for collection mixins (see issue #2531) Adding testing for adherence to collections.abc protocols for the following classes: Array, List, ImmutableArray, ImmutableList, Dictionary, ImmutableDictionary and ReadOnlyDictionary Tests for Python list and dict are also present as a reference but commented out. --- tests/test_collection_mixins_mapping.py | 252 +++++++++++++++++++++ tests/test_collection_mixins_sequence.py | 276 +++++++++++++++++++++++ 2 files changed, 528 insertions(+) create mode 100644 tests/test_collection_mixins_mapping.py create mode 100644 tests/test_collection_mixins_sequence.py diff --git a/tests/test_collection_mixins_mapping.py b/tests/test_collection_mixins_mapping.py new file mode 100644 index 000000000..b11dab252 --- /dev/null +++ b/tests/test_collection_mixins_mapping.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +import operator +from typing import Any +from collections.abc import Container + +import pytest + +import clr +clr.AddReference('System.Collections.Immutable') +from System import Nullable, Object, ValueType, Int32, String +import System.Collections.Generic as Gen +import System.Collections.Immutable as Imm +import System.Collections.ObjectModel as OM + +kv_pairs_1 = ((0, "0"), (10, "10"), (20, "20"), (30, "30")) +kv_pairs_2 = kv_pairs_1 + ((40, None), ) +kv_pairs_3 = (("0", 0), ("10", 10), ("20", 20), ("30", 30)) +kv_pairs_4 = kv_pairs_3 + (("40", None), ) + + +def exactly_equal(val1, val2): + return type(val1) is type(val2) and val1 == val2, '{0!r} != {1!r}'.format(val1, val2) + + +def translate_pytype(pytype, *, nullable=False): + if pytype is int: + cstype = Int32 + elif pytype is str: + cstype = String + else: + raise NotImplementedError('Unsupported type: {0!r}'.format(pytype)) + + if nullable and issubclass(cstype, ValueType): + cstype = Nullable[cstype] + + return cstype + + +class MappingTests: + ktype: type + vtype: type + nullable: bool + cs_ktype: type + cs_vtype: type + mapped_to_null: Container[Any] + dct: Any + + def __init_subclass__(cls, /, values=None, **kwargs): + if values is not None: + cls.ktype,cls.vtype,cls.nullable,cls.cs_ktype,cls.cs_vtype = cls.deduce_types(values) + cls.mapped_to_null = tuple(key for key,val in values if val is None) + + @staticmethod + def deduce_types(values): + (ktype, ) = {type(key) for key,_ in values} + vtypes = {type(val) for _,val in values} + nullable = type(None) in vtypes + if nullable: + vtypes.remove(type(None)) + (vtype, ) = vtypes + cs_ktype = translate_pytype(ktype) + cs_vtype = translate_pytype(vtype, nullable=nullable) + return (ktype, vtype, nullable, cs_ktype, cs_vtype) + + def test_len(self): + assert type(len(self.dct)) is int + assert len(self.dct) == 4 if not self.nullable else 5 + + def test_iter(self): + for idx,key in enumerate(self.dct): + assert type(key) is self.ktype and int(key) == idx * 10 + + def test_keys(self): + keys = sorted(self.dct.keys()) +# print(f'### {list(self.dct.keys())}') # TODO: DELME + for idx,key in enumerate(keys): + assert exactly_equal(key, self.ktype(idx * 10)) + + def test_values(self): + values = sorted(self.dct.values(), key=lambda val: val if val is not None else self.vtype(999)) + for idx,val in enumerate(values): + exp_val = None if self.ktype(10 * idx) in self.mapped_to_null else self.vtype(10 * idx) + assert exactly_equal(val, exp_val) + + def test_items(self): + items = sorted(self.dct.items(), key=operator.itemgetter(0)) + for idx,tpl in enumerate(items): + assert type(tpl) is tuple and len(tpl) == 2 + key,val = tpl + assert exactly_equal(key, self.ktype(idx * 10)) + exp_val = None if self.ktype(10 * idx) in self.mapped_to_null else self.vtype(10 * idx) + assert exactly_equal(val, exp_val) + + def test_contains(self): + assert self.ktype(10) in self.dct + assert self.ktype(50) not in self.dct + assert 12.34 not in self.dct + + def test_getitem(self): + for idx in range(len(self.dct)): + val = self.dct[self.ktype(10 * idx)] + assert exactly_equal(val, self.vtype(10 * idx)) + + def test_getitem_raise(self): + with pytest.raises(KeyError): + self.dct[self.ktype(50)] + with pytest.raises(KeyError): + self.dct[12.34] + + def test_get(self): + val = self.dct.get(self.ktype(10)) + assert exactly_equal(val, self.vtype(10)) + assert self.dct.get(self.ktype(50)) is None + val = self.dct.get(self.ktype(50), 123.1) + assert val == 123.1 + + +class MutableMappingTests(MappingTests): + def get_copy(self) -> Any: + raise NotImplementedError('must be overridden!') + + def test_setitem(self): + dct = self.get_copy() + key,val = (self.ktype(10), self.vtype(11)) + dct[key] = val + assert exactly_equal(dct[key], val) + + def test_setitem_raise(self): + if isinstance(self.dct, Object): # this is only relevant for CLR types + dct = self.get_copy() + with pytest.raises(Exception): + dct[12.34] = self.vtype(0) + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_delitem(self): + dct = self.get_copy() + del dct[self.ktype(10)] + assert self.ktype(10) not in dct + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_delitem_raise(self): + dct = self.get_copy() + with pytest.raises(KeyError): + del dct[12.34] + + def test_pop(self): + dct = self.get_copy() + length = len(dct) + val = dct.pop(self.ktype(10)) + assert exactly_equal(val, self.vtype(10)) + val = dct.pop(self.ktype(10), self.vtype(11)) + assert exactly_equal(val, self.vtype(11)) + assert len(dct) == length - 1 + + def test_popitem(self): + dct = self.get_copy() + while len(dct) != 0: + tpl = dct.popitem() + assert type(tpl) is tuple and len(tpl) == 2 + key,val = tpl + assert type(key) is self.ktype + assert type(val) is self.vtype or (self.nullable and val is None) + if val is not None: + assert int(key) == int(val) + + def test_clear(self): + dct = self.get_copy() + dct.clear() + assert len(dct) == 0 + assert dict(dct) == {} + + def test_setdefault(self): + dct = self.get_copy() + dct.setdefault(self.ktype(50), self.vtype(50)) + assert exactly_equal(dct[self.ktype(50)], self.vtype(50)) + + def test_update(self): + dct = self.get_copy() + pydict = {self.ktype(num): self.vtype(num) for num in (30, 40)} + if self.nullable: + pydict[self.ktype(50)] = None + dct.update(pydict) + pydict.update({self.ktype(num): self.vtype(num) for num in (0, 10, 20)}) # put in the items we expect to be set already + assert dict(dct) == pydict + extra_vals = tuple((self.ktype(num), self.vtype(num)) for num in (60, 70)) + dct.update(extra_vals) + pydict.update(extra_vals) + assert dict(dct) == pydict + if self.ktype is str: + dct.update(aaa=80, bbb=90) + pydict.update(aaa=80, bbb=90) + assert dict(dct) == pydict + + +class PyDictTests(MutableMappingTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.dct = dict(values) + + def get_copy(self): + return self.dct.copy() + +# class TestPyDictIntStr (PyDictTests, values=kv_pairs_1): pass +# class TestPyDictIntNullStr(PyDictTests, values=kv_pairs_2): pass +# class TestPyDictStrInt (PyDictTests, values=kv_pairs_3): pass +# class TestPyDictStrNullInt(PyDictTests, values=kv_pairs_4): pass + + +def make_cs_dictionary(cs_ktype, cs_vtype, values): + dct = Gen.Dictionary[cs_ktype, cs_vtype]() + for key,val in values: + dct[key] = None if val is None else val + return dct + + +class DictionaryTests(MutableMappingTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) + + def get_copy(self): + return Gen.Dictionary[self.cs_ktype, self.cs_vtype](self.dct) + +class TestDictionaryIntStr (DictionaryTests, values=kv_pairs_1): pass +class TestDictionaryIntNullStr(DictionaryTests, values=kv_pairs_2): pass +class TestDictionaryStrInt (DictionaryTests, values=kv_pairs_3): pass +class TestDictionaryStrNullInt(DictionaryTests, values=kv_pairs_4): pass + + +class ReadOnlyDictionaryTests(MappingTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) + cls.dct = OM.ReadOnlyDictionary[cls.cs_ktype, cls.cs_vtype](dct) + +class ReadOnlyDictionaryIntStr (ReadOnlyDictionaryTests, values=kv_pairs_1): pass +class ReadOnlyDictionaryIntNullStr(ReadOnlyDictionaryTests, values=kv_pairs_2): pass +class ReadOnlyDictionaryStrInt (ReadOnlyDictionaryTests, values=kv_pairs_3): pass +class ReadOnlyDictionaryStrNullInt(ReadOnlyDictionaryTests, values=kv_pairs_4): pass + + +class ImmutableDictionaryTests(MappingTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) + cls.dct = Imm.ImmutableDictionary.ToImmutableDictionary[cls.cs_ktype, cls.cs_vtype](dct) + +class TestImmutableDictionaryIntStr (ImmutableDictionaryTests, values=kv_pairs_1): pass +class TestImmutableDictionaryIntNullStr(ImmutableDictionaryTests, values=kv_pairs_2): pass +class TestImmutableDictionaryStrInt (ImmutableDictionaryTests, values=kv_pairs_3): pass +class TestImmutableDictionaryStrNullInt(ImmutableDictionaryTests, values=kv_pairs_4): pass diff --git a/tests/test_collection_mixins_sequence.py b/tests/test_collection_mixins_sequence.py new file mode 100644 index 000000000..9c836044c --- /dev/null +++ b/tests/test_collection_mixins_sequence.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +from typing import Any +from collections.abc import Container + +import pytest + +import clr +clr.AddReference('System.Collections.Immutable') +from System import Array, Nullable, ValueType, Int32, String +import System.Collections.Generic as Gen +import System.Collections.Immutable as Imm + +values_1 = (10, 11, 12, 13, 14) +values_2 = values_1 + (None, ) +values_3 = tuple(map(str, values_1)) +values_4 = values_3 + (None, ) + + +def exactly_equal(val1, val2): + return type(val1) is type(val2) and val1 == val2, '{0!r} != {1!r}'.format(val1, val2) + + +def translate_pytype(pytype, *, nullable=False): + if pytype is int: + cstype = Int32 + elif pytype is str: + cstype = String + else: + raise NotImplementedError('Unsupported type: {0!r}'.format(pytype)) + + if nullable and issubclass(cstype, ValueType): + cstype = Nullable[cstype] + + return cstype + + +class SequenceTests: + vtype: type + nullable: bool + cs_vtype: type + lst: Any + null_values: Container[int] + + def __init_subclass__(cls, /, values=None, **kwargs): + if values is not None: + cls.vtype,cls.nullable,cls.cs_vtype = cls.deduce_types(values) + cls.null_values = tuple(idx for idx,val in enumerate(values) if val is None) + + @staticmethod + def deduce_types(values): + vtypes = set(map(type, values)) + nullable = type(None) in vtypes + if nullable: + vtypes.remove(type(None)) + (vtype, ) = vtypes + cs_vtype = translate_pytype(vtype, nullable=nullable) + return (vtype, nullable, cs_vtype) + + def test_len(self): + length = len(self.lst) + assert exactly_equal(length, 5) + + def test_iter(self): + for idx,val in enumerate(self.lst): + exp_val = None if idx in self.null_values else self.vtype(idx + 10) + assert exactly_equal(val, exp_val) + + def test_reversed(self): + length = len(self.lst) + for idx,val in enumerate(reversed(self.lst)): + exp_val = None if idx in self.null_values else self.vtype(10 + length - idx - 1) + assert exactly_equal(val, exp_val) + + def test_getitem(self): + length = len(self.lst) + for idx in range(length): + val = self.lst[idx] + assert exactly_equal(val, self.vtype(10 + idx)) + + def test_getitem_negidx(self): + length = len(self.lst) + for idx in range(length): + val = self.lst[idx - length] + assert exactly_equal(val, self.vtype(10 + idx)) + + def test_getitem_raise(self): + length = len(self.lst) + with pytest.raises(IndexError): + self.lst[length] + with pytest.raises(IndexError): + self.lst[-1-length] + + def test_contains(self): + assert self.vtype(10) in self.lst + assert self.vtype(14) in self.lst + assert self.vtype(15) not in self.lst + + def test_index(self): + assert self.lst.index(self.vtype(10)) == 0 + assert self.lst.index(self.vtype(14)) == 4 + with pytest.raises(ValueError): + self.lst.index(self.vtype(15)) + + +class MutableSequenceTests(SequenceTests): + def get_copy(self) -> Any: + raise NotImplementedError('must be overridden!') + + def test_setitem(self): + arr = self.get_copy() + arr[0] = self.vtype(111) + assert exactly_equal(arr[0], self.vtype(111)) + + def test_setitem_negidx(self): + arr = self.get_copy() + arr[-1] = self.vtype(222) + assert arr[len(arr)-1] == self.vtype(222) + + def test_setitem_raise(self): + arr = self.get_copy() + length = len(arr) + with pytest.raises(IndexError): + arr[length] = self.vtype(0) + with pytest.raises(IndexError): + arr[-1-length] = self.vtype(0) + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_delitem(self): + arr = self.get_copy() + exp_lst = list(arr)[:-1] + del arr[-1] + assert list(arr) == exp_lst + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_delitem_raise(self): + arr = self.get_copy() + with pytest.raises(Exception): + del arr[len(arr)] + with pytest.raises(Exception): + del arr[-1-len(arr)] + + def test_insert(self): + arr = self.get_copy() + length = len(arr) + arr.insert(1, 333) + assert len(arr) == length + 1 + assert arr[1] == 333 + + def test_append(self): + arr = self.get_copy() + orig_length = len(arr) + arr.append(444) + assert len(arr) == orig_length + 1 + assert arr[orig_length] == 444 + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_pop(self): + arr = self.get_copy() + length = len(arr) + val = arr.pop(1) + assert exactly_equal(val, self.vtype(11)) + assert len(arr) == length - 1 + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_pop_last(self): + arr = self.get_copy() + length = len(arr) + val = arr.pop() + assert exactly_equal(val, None if self.nullable else self.vtype(14)) + assert len(arr) == length - 1 + + def test_extend(self): + arr = self.get_copy() + orig_length = len(arr) + arr.extend([self.vtype(555), self.vtype(666)]) + assert exactly_equal(arr[orig_length ], self.vtype(555)) + assert exactly_equal(arr[orig_length + 1], self.vtype(666)) + assert len(arr) == orig_length + 2 + + def test_reverse(self): + arr = self.get_copy() + expected = list(arr)[::-1] + arr.reverse() + assert list(arr) == expected + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_remove(self): + arr = self.get_copy() + expected = [val for val in arr if val != self.vtype(13)] + arr.remove(self.vtype(13)) + assert list(arr) == expected + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_remove_raise(self): + arr = self.get_copy() + with pytest.raises(ValueError): + arr.remove(self.vtype(15)) + + def test_iadd(self): + arr = self.get_copy() + orig_length = len(arr) + arr += [777, 888] + assert exactly_equal(arr[orig_length ], self.vtype(777)) + assert exactly_equal(arr[orig_length + 1], self.vtype(888)) + assert len(arr) == orig_length + 2 + + @pytest.mark.xfail(reason='Known to crash', run=False) + def test_clear(self): + arr = self.get_copy() + arr.clear() + assert len(arr) == 0 + + +class PyListTests(MutableSequenceTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.lst = list(values) + + def get_copy(self): + return self.lst.copy() + +# class TestPyListInt (PyListTests, values=values_1): pass +# class TestPyListNullInt(PyListTests, values=values_2): pass +# class TestPyListStr (PyListTests, values=values_3): pass +# class TestPyListNullStr(PyListTests, values=values_4): pass + + +class ArrayTests(MutableSequenceTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.lst = Array[cls.cs_vtype](values) + + def get_copy(self): + return Array[self.cs_vtype](self.lst) + +class TestArrayInt (ArrayTests, values=values_1): pass +class TestArrayNullInt(ArrayTests, values=values_2): pass +class TestArrayStr (ArrayTests, values=values_3): pass +class TestArrayNullStr(ArrayTests, values=values_4): pass + + +class ImmutableArrayTests(SequenceTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.lst = Imm.ImmutableArray[cls.cs_vtype](values) + +class TestImmutableArrayInt (ImmutableArrayTests, values=values_1): pass +class TestImmutableArrayNullInt(ImmutableArrayTests, values=values_2): pass +class TestImmutableArrayStr (ImmutableArrayTests, values=values_3): pass +class TestImmutableArrayNullStr(ImmutableArrayTests, values=values_4): pass + + +class ListTests(MutableSequenceTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.lst = Gen.List[cls.cs_vtype](Array[cls.cs_vtype](values)) + + def get_copy(self): + return Gen.List[self.cs_vtype](self.lst) + +class TestListInt (ListTests, values=values_1): pass +class TestListNullInt(ListTests, values=values_2): pass +class TestListStr (ListTests, values=values_3): pass +class TestListNullStr(ListTests, values=values_4): pass + + +class ImmutableListTests(SequenceTests): + def __init_subclass__(cls, /, values, **kwargs): + super().__init_subclass__(values=values, **kwargs) + cls.lst = Imm.ImmutableList.ToImmutableList[cls.cs_vtype](Array[cls.cs_vtype](values)) + +class TestImmutableListInt (ImmutableListTests, values=values_1): pass +class TestImmutableListNullInt(ImmutableListTests, values=values_2): pass +class TestImmutableListStr (ImmutableListTests, values=values_3): pass +class TestImmutableListNullStr(ImmutableListTests, values=values_4): pass From 4f821cea5d61134cc252c433941c5d2bf4f02067 Mon Sep 17 00:00:00 2001 From: Diego Baldassar Date: Fri, 10 Jan 2025 15:31:02 +0100 Subject: [PATCH 2/2] Added missing type conversions in mixin tests Also made sequence __deitem__ test not rely on negative indexes --- tests/test_collection_mixins_sequence.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_collection_mixins_sequence.py b/tests/test_collection_mixins_sequence.py index 9c836044c..f10922c33 100644 --- a/tests/test_collection_mixins_sequence.py +++ b/tests/test_collection_mixins_sequence.py @@ -129,7 +129,7 @@ def test_setitem_raise(self): def test_delitem(self): arr = self.get_copy() exp_lst = list(arr)[:-1] - del arr[-1] + del arr[len(arr) - 1] assert list(arr) == exp_lst @pytest.mark.xfail(reason='Known to crash', run=False) @@ -143,16 +143,16 @@ def test_delitem_raise(self): def test_insert(self): arr = self.get_copy() length = len(arr) - arr.insert(1, 333) + arr.insert(1, self.vtype(333)) assert len(arr) == length + 1 - assert arr[1] == 333 + assert arr[1] == self.vtype(333) def test_append(self): arr = self.get_copy() orig_length = len(arr) - arr.append(444) + arr.append(self.vtype(444)) assert len(arr) == orig_length + 1 - assert arr[orig_length] == 444 + assert arr[orig_length] == self.vtype(444) @pytest.mark.xfail(reason='Known to crash', run=False) def test_pop(self): @@ -200,7 +200,7 @@ def test_remove_raise(self): def test_iadd(self): arr = self.get_copy() orig_length = len(arr) - arr += [777, 888] + arr += [self.vtype(777), self.vtype(888)] assert exactly_equal(arr[orig_length ], self.vtype(777)) assert exactly_equal(arr[orig_length + 1], self.vtype(888)) assert len(arr) == orig_length + 2