aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/utils/tests
diff options
context:
space:
mode:
authorPhilip Paquette <pcpaquette@gmail.com>2018-09-26 07:48:55 -0400
committerPhilip Paquette <pcpaquette@gmail.com>2019-04-18 11:14:24 -0400
commit6187faf20384b0c5a4966343b2d4ca47f8b11e45 (patch)
tree151ccd21aea20180432c13fe4b58240d3d9e98b6 /diplomacy/utils/tests
parent96b7e2c03ed98705754f13ae8efa808b948ee3a8 (diff)
Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License
Diffstat (limited to 'diplomacy/utils/tests')
-rw-r--r--diplomacy/utils/tests/__init__.py16
-rw-r--r--diplomacy/utils/tests/test_common.py147
-rw-r--r--diplomacy/utils/tests/test_jsonable.py81
-rw-r--r--diplomacy/utils/tests/test_jsonable_changes.py189
-rw-r--r--diplomacy/utils/tests/test_parsing.py307
-rw-r--r--diplomacy/utils/tests/test_priority_dict.py102
-rw-r--r--diplomacy/utils/tests/test_sorted_dict.py154
-rw-r--r--diplomacy/utils/tests/test_sorted_set.py168
-rw-r--r--diplomacy/utils/tests/test_time.py77
9 files changed, 1241 insertions, 0 deletions
diff --git a/diplomacy/utils/tests/__init__.py b/diplomacy/utils/tests/__init__.py
new file mode 100644
index 0000000..4f2769f
--- /dev/null
+++ b/diplomacy/utils/tests/__init__.py
@@ -0,0 +1,16 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
diff --git a/diplomacy/utils/tests/test_common.py b/diplomacy/utils/tests/test_common.py
new file mode 100644
index 0000000..a1c303d
--- /dev/null
+++ b/diplomacy/utils/tests/test_common.py
@@ -0,0 +1,147 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test for diplomacy network code utils. """
+import ujson as json
+
+from diplomacy.utils import common, exceptions
+
+def assert_raises(callback, expected_exceptions):
+ """ Checks that given callback raises given exceptions. """
+
+ try:
+ callback()
+ except expected_exceptions:
+ pass
+ else:
+ raise AssertionError('Should fail %s %s' % (callback, str(expected_exceptions)))
+
+def assert_equals(expected, computed):
+ """ Checks that expect == computed. """
+
+ if expected != computed:
+ raise AssertionError('\nExpected:\n=========\n%s\n\nComputed:\n=========\n%s\n' % (expected, computed))
+
+def test_hash_password():
+ """ Test passwords hashing. Note: slower than the other tests. """
+
+ password1 = '123456789'
+ password2 = 'abcdef'
+ password_unicode = 'しろいねこをみた。 白い猫を見た。'
+ for password in (password1, password2, password_unicode):
+ hashed_password = common.hash_password(password)
+ json_hashed_password = json.dumps(common.hash_password(password))
+ hashed_password_from_json = json.loads(json_hashed_password)
+ # It seems hashed passwords are not necessarily the same for 2 different calls to hash function.
+ assert common.is_valid_password(password, hashed_password), (password, hashed_password)
+ assert common.is_valid_password(password, hashed_password_from_json), (password, hashed_password_from_json)
+
+def test_generate_token():
+ """ Test token generation. """
+
+ for n_bytes in (128, 344):
+ token = common.generate_token(n_bytes)
+ assert isinstance(token, str) and len(token) == 2 * n_bytes
+
+def test_is_sequence():
+ """ Test sequence type checking function. """
+
+ assert common.is_sequence((1, 2, 3))
+ assert common.is_sequence([1, 2, 3])
+ assert common.is_sequence({1, 2, 3})
+ assert common.is_sequence(())
+ assert common.is_sequence([])
+ assert common.is_sequence(set())
+ assert not common.is_sequence('i am a string')
+ assert not common.is_sequence({})
+ assert not common.is_sequence(1)
+ assert not common.is_sequence(False)
+ assert not common.is_sequence(-2.5)
+
+def test_is_dictionary():
+ """ Test dictionary type checking function. """
+
+ assert common.is_dictionary({'a': 1, 'b': 2})
+ assert not common.is_dictionary((1, 2, 3))
+ assert not common.is_dictionary([1, 2, 3])
+ assert not common.is_dictionary({1, 2, 3})
+
+ assert not common.is_dictionary(())
+ assert not common.is_dictionary([])
+ assert not common.is_dictionary(set())
+
+ assert not common.is_dictionary('i am a string')
+
+def test_camel_to_snake_case():
+ """ Test conversion from camel case to snake case. """
+
+ for camel, expected_snake in [
+ ('a', 'a'),
+ ('A', 'a'),
+ ('AA', 'a_a'),
+ ('AbCdEEf', 'ab_cd_e_ef'),
+ ('Aa', 'aa'),
+ ('OnGameDone', 'on_game_done'),
+ ('AbstractSuperClass', 'abstract_super_class'),
+ ('ABCDEFghikKLm', 'a_b_c_d_e_fghik_k_lm'),
+ ('is_a_thing', 'is_a_thing'),
+ ('A_a_Aa__', 'a_a_aa__'),
+ ('Horrible_SuperClass_nameWith_mixedSyntax', 'horrible_super_class_name_with_mixed_syntax'),
+ ]:
+ computed_snake = common.camel_case_to_snake_case(camel)
+ assert computed_snake == expected_snake, ('camel : expected : computed:', camel, expected_snake, computed_snake)
+
+def test_snake_to_camel_case():
+ """ Test conversion from snake case to camel upper case. """
+
+ for expected_camel, snake in [
+ ('A', 'a'),
+ ('AA', 'a_a'),
+ ('AbCdEEf', 'ab_cd_e_ef'),
+ ('Aa', 'aa'),
+ ('OnGameDone', 'on_game_done'),
+ ('AbstractSuperClass', 'abstract_super_class'),
+ ('ABCDEFghikKLm', 'a_b_c_d_e_fghik_k_lm'),
+ ('IsAThing', 'is_a_thing'),
+ ('AAAa__', 'a_a_aa__'),
+ ('_AnHorrible_ClassName', '__an_horrible__class_name'),
+ ]:
+ computed_camel = common.snake_case_to_upper_camel_case(snake)
+ assert computed_camel == expected_camel, ('snake : expected : computed:', snake, expected_camel, computed_camel)
+
+def test_assert_no_common_keys():
+ """ Test dictionary disjunction checking function. """
+
+ dct1 = {'a': 1, 'b': 2, 'c': 3}
+ dct2 = {'a': 1, 'e': 2, 'd': 3}
+ dct3 = {'m': 1, 'e': 2, 'f': 3}
+ common.assert_no_common_keys(dct1, dct3)
+ assert_raises(lambda: common.assert_no_common_keys(dct1, dct2), exceptions.CommonKeyException)
+ assert_raises(lambda: common.assert_no_common_keys(dct2, dct3), exceptions.CommonKeyException)
+
+def test_timestamp():
+ """ Test timestamp generation. """
+
+ timestamp1 = common.timestamp_microseconds()
+ timestamp2 = common.timestamp_microseconds()
+ timestamp3 = common.timestamp_microseconds()
+ assert isinstance(timestamp1, int)
+ assert isinstance(timestamp2, int)
+ assert isinstance(timestamp3, int)
+ assert timestamp1 > 1e6
+ assert timestamp2 > 1e6
+ assert timestamp3 > 1e6
+ assert timestamp1 <= timestamp2 <= timestamp3, (timestamp1, timestamp2, timestamp3)
diff --git a/diplomacy/utils/tests/test_jsonable.py b/diplomacy/utils/tests/test_jsonable.py
new file mode 100644
index 0000000..73d65c1
--- /dev/null
+++ b/diplomacy/utils/tests/test_jsonable.py
@@ -0,0 +1,81 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test Jsonable. """
+import ujson as json
+
+from diplomacy.utils import parsing
+from diplomacy.utils.jsonable import Jsonable
+from diplomacy.utils.sorted_dict import SortedDict
+from diplomacy.utils.sorted_set import SortedSet
+
+class MyJsonable(Jsonable):
+ """ Example of class derived from Jsonable. """
+ __slots__ = ('field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'field_f', 'field_g')
+
+ model = {
+ 'field_a': bool,
+ 'field_b': str,
+ 'field_c': parsing.OptionalValueType(float),
+ 'field_d': parsing.DefaultValueType(str, 'super'),
+ 'field_e': parsing.SequenceType(int),
+ 'field_f': parsing.SequenceType(float, sequence_builder=SortedSet.builder(float)),
+ 'field_g': parsing.DefaultValueType(parsing.DictType(str, int, SortedDict.builder(str, int)), {'x': -1})
+ }
+
+ def __init__(self, **kwargs):
+ """ Constructor """
+ self.field_a = None
+ self.field_b = None
+ self.field_c = None
+ self.field_d = None
+ self.field_e = None
+ self.field_f = None
+ self.field_g = {}
+ super(MyJsonable, self).__init__(**kwargs)
+
+def test_jsonable_parsing():
+ """ Test parsing for Jsonable. """
+
+ attributes = ('field_a', 'field_b', 'field_c', 'field_d', 'field_e', 'field_f', 'field_g')
+
+ # Building and validating
+ my_jsonable = MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5])
+ for attribute_name in attributes:
+ assert hasattr(my_jsonable, attribute_name)
+ assert isinstance(my_jsonable.field_a, bool)
+ assert isinstance(my_jsonable.field_b, str)
+ assert my_jsonable.field_c is None
+ assert isinstance(my_jsonable.field_d, str), my_jsonable.field_d
+ assert isinstance(my_jsonable.field_e, list)
+ assert isinstance(my_jsonable.field_f, SortedSet)
+ assert isinstance(my_jsonable.field_g, SortedDict)
+ assert my_jsonable.field_d == 'super'
+ assert my_jsonable.field_e == [1]
+ assert my_jsonable.field_f == SortedSet(float, (6.5,))
+ assert len(my_jsonable.field_g) == 1 and my_jsonable.field_g['x'] == -1
+
+ # Building from its json representation and validating
+ from_json = MyJsonable.from_dict(json.loads(json.dumps(my_jsonable.to_dict())))
+ for attribute_name in attributes:
+ assert hasattr(from_json, attribute_name), attribute_name
+ assert from_json.field_a == my_jsonable.field_a
+ assert from_json.field_b == my_jsonable.field_b
+ assert from_json.field_c == my_jsonable.field_c
+ assert from_json.field_d == my_jsonable.field_d
+ assert from_json.field_e == my_jsonable.field_e
+ assert from_json.field_f == my_jsonable.field_f
+ assert from_json.field_g == my_jsonable.field_g
diff --git a/diplomacy/utils/tests/test_jsonable_changes.py b/diplomacy/utils/tests/test_jsonable_changes.py
new file mode 100644
index 0000000..992a8fb
--- /dev/null
+++ b/diplomacy/utils/tests/test_jsonable_changes.py
@@ -0,0 +1,189 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test changes in a Jsonable schema. """
+#pylint: disable=invalid-name
+from diplomacy.utils import parsing
+from diplomacy.utils.jsonable import Jsonable
+
+def converter_to_int(val):
+ """ A converter from given value to an integer. Used in Version1. """
+ try:
+ return int(val)
+ except ValueError:
+ return 0
+
+class Version1(Jsonable):
+ """ A Jsonable with fields a, b, c, d.
+ NB: To parse a dict from Version22 to Version1, modified fields a and c must be convertible in Version1.
+ using ConverterType in Version1.
+ """
+ model = {
+ 'a': parsing.ConverterType(int, converter_to_int),
+ 'b': parsing.OptionalValueType(str),
+ 'c': parsing.ConverterType(float, converter_function=float),
+ 'd': parsing.DefaultValueType(bool, True),
+ }
+
+ def __init__(self, **kwargs):
+ self.a = None
+ self.b = None
+ self.c = None
+ self.d = None
+ super(Version1, self).__init__(**kwargs)
+
+class Version20(Jsonable):
+ """ Version1 with removed fields b and d.
+ NB: To parse a dict from Version20 to Version1, removed fields b and d must be optional in Version1.
+ """
+ model = {
+ 'a': int,
+ 'c': float,
+ }
+
+ def __init__(self, **kwargs):
+ self.a = None
+ self.c = None
+ super(Version20, self).__init__(**kwargs)
+
+class Version21(Jsonable):
+ """ Version1 with added fields e and f.
+ NB: To parse a dict from Version1 to Version21, added fields e and f must be optional in Version21.
+ """
+ model = {
+ 'a': int,
+ 'b': str,
+ 'c': float,
+ 'd': bool,
+ 'e': parsing.DefaultValueType(parsing.EnumerationType([100, 200, 300, 400]), 100),
+ 'f': parsing.DefaultValueType(dict, {'x': 1, 'y': 2})
+ }
+
+ def __init__(self, **kwargs):
+ self.a = None
+ self.b = None
+ self.c = None
+ self.d = None
+ self.e = None
+ self.f = {}
+ super(Version21, self).__init__(**kwargs)
+
+class Version22(Jsonable):
+ """ Version1 with modified types for a and c.
+ NB: To parse a dict from Version1 to Version22, modified fields a and c must be convertible
+ using ConverterType in Version22.
+ """
+ model = {
+ 'a': parsing.ConverterType(str, converter_function=str),
+ 'b': str,
+ 'c': parsing.ConverterType(bool, converter_function=bool),
+ 'd': bool,
+ }
+
+ def __init__(self, **kwargs):
+ self.a = None
+ self.b = None
+ self.c = None
+ self.d = None
+ super(Version22, self).__init__(**kwargs)
+
+class Version3(Jsonable):
+ """ Version 1 with a modified, b removed, e added.
+ To parse a dict between Version3 and Version1:
+ - a must be convertible in both versions.
+ - b must be optional in Version1.
+ - e must be optional in Version3.
+ """
+ model = {
+ 'a': parsing.ConverterType(str, converter_function=str),
+ 'c': float,
+ 'd': bool,
+ 'e': parsing.OptionalValueType(parsing.SequenceType(int))
+ }
+
+ def __init__(self, **kwargs):
+ self.a = None
+ self.c = None
+ self.d = None
+ self.e = None
+ super(Version3, self).__init__(**kwargs)
+
+def test_jsonable_changes_v1_v20():
+ """ Test changes from Version1 to Version20. """
+ v20 = Version20(a=1, c=1.5)
+ v1 = Version1(a=1, b='b', c=1.5, d=False)
+ json_v1 = v1.to_dict()
+ v20_from_v1 = Version20.from_dict(json_v1)
+ json_v20_from_v1 = v20_from_v1.to_dict()
+ v1_from_v20_from_v1 = Version1.from_dict(json_v20_from_v1)
+ assert v1_from_v20_from_v1.b is None
+ assert v1_from_v20_from_v1.d is True
+ json_v20 = v20.to_dict()
+ v1_from_v20 = Version1.from_dict(json_v20)
+ assert v1_from_v20.b is None
+ assert v1_from_v20.d is True
+
+def test_jsonable_changes_v1_v21():
+ """ Test changes from Version1 to Version21. """
+ v21 = Version21(a=1, b='b21', c=1.5, d=True, e=300, f=dict(x=1, y=2))
+ v1 = Version1(a=1, b='b', c=1.5, d=False)
+ json_v1 = v1.to_dict()
+ v21_from_v1 = Version21.from_dict(json_v1)
+ assert v21_from_v1.e == 100
+ assert v21_from_v1.f['x'] == 1
+ assert v21_from_v1.f['y'] == 2
+ json_v21_from_v1 = v21_from_v1.to_dict()
+ v1_from_v21_from_v1 = Version1.from_dict(json_v21_from_v1)
+ assert v1_from_v21_from_v1.b == 'b'
+ assert v1_from_v21_from_v1.d is False
+ json_v21 = v21.to_dict()
+ v1_from_v21 = Version1.from_dict(json_v21)
+ assert v1_from_v21.b == 'b21'
+ assert v1_from_v21.d is True
+
+def test_jsonable_changes_v1_v22():
+ """ Test changes from Version1 to Version22. """
+ v22 = Version22(a='a', b='b', c=False, d=False)
+ v1 = Version1(a=1, b='b', c=1.5, d=False)
+ json_v1 = v1.to_dict()
+ v22_from_v1 = Version22.from_dict(json_v1)
+ assert v22_from_v1.a == '1'
+ assert v22_from_v1.c is True
+ json_v22_from_v1 = v22_from_v1.to_dict()
+ v1_from_v22_from_v1 = Version1.from_dict(json_v22_from_v1)
+ assert v1_from_v22_from_v1.a == 1
+ assert v1_from_v22_from_v1.c == 1.0
+ json_v22 = v22.to_dict()
+ v1_from_v22 = Version1.from_dict(json_v22)
+ assert v1_from_v22.a == 0
+ assert v1_from_v22.c == 0.0
+
+def test_jsonable_changes_v1_v3():
+ """ Test changes from Version1 to Version3. """
+ v3 = Version3(a='a', c=1.5, d=False, e=(1, 2, 3))
+ v1 = Version1(a=1, b='b', c=1.5, d=False)
+ json_v1 = v1.to_dict()
+ v3_from_v1 = Version3.from_dict(json_v1)
+ assert v3_from_v1.a == '1'
+ assert v3_from_v1.e is None
+ json_v3_from_v1 = v3_from_v1.to_dict()
+ v1_from_v3_from_v1 = Version1.from_dict(json_v3_from_v1)
+ assert v1_from_v3_from_v1.a == 1
+ assert v1_from_v3_from_v1.b is None
+ json_v3 = v3.to_dict()
+ v1_from_v3 = Version1.from_dict(json_v3)
+ assert v1_from_v3.a == 0
+ assert v1_from_v3.b is None
diff --git a/diplomacy/utils/tests/test_parsing.py b/diplomacy/utils/tests/test_parsing.py
new file mode 100644
index 0000000..f64ad26
--- /dev/null
+++ b/diplomacy/utils/tests/test_parsing.py
@@ -0,0 +1,307 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test module parsing. """
+from diplomacy.utils import exceptions, parsing
+from diplomacy.utils.sorted_dict import SortedDict
+from diplomacy.utils.sorted_set import SortedSet
+from diplomacy.utils.tests.test_common import assert_raises
+from diplomacy.utils.tests.test_jsonable import MyJsonable
+
+class MyStringable():
+ """ Example of Stringable class.
+ As instances of such class may be used as dict keys, class should define a proper __hash__().
+ """
+
+ def __init__(self, value):
+ self.attribute = str(value)
+
+ def __str__(self):
+ return 'MyStringable %s' % self.attribute
+
+ def __hash__(self):
+ return hash(self.attribute)
+
+ def __eq__(self, other):
+ return isinstance(other, MyStringable) and self.attribute == other.attribute
+
+ def __lt__(self, other):
+ return isinstance(other, MyStringable) and self.attribute < other.attribute
+
+ @staticmethod
+ def from_string(str_repr):
+ """ Converts a string representation `str_repr` of MyStringable to an instance of MyStringable. """
+ return MyStringable(str_repr[len('MyStringable '):])
+
+def test_default_value_type():
+ """ Test default value type. """
+
+ for default_value in (True, False, None):
+ checker = parsing.DefaultValueType(bool, default_value)
+ assert_raises(lambda ch=checker: ch.validate(1), exceptions.TypeException)
+ assert_raises(lambda ch=checker: ch.validate(1.1), exceptions.TypeException)
+ assert_raises(lambda ch=checker: ch.validate(''), exceptions.TypeException)
+ for value in (True, False, None):
+ checker.validate(value)
+ if value is None:
+ assert checker.to_type(value) is default_value
+ assert checker.to_json(value) is default_value
+ else:
+ assert checker.to_type(value) is value
+ assert checker.to_json(value) is value
+ assert checker.update(None) is default_value
+
+def test_optional_value_type():
+ """ Test optional value type. """
+
+ checker = parsing.OptionalValueType(bool)
+ assert_raises(lambda ch=checker: ch.validate(1), exceptions.TypeException)
+ assert_raises(lambda ch=checker: ch.validate(1.1), exceptions.TypeException)
+ assert_raises(lambda ch=checker: ch.validate(''), exceptions.TypeException)
+ for value in (True, False, None):
+ checker.validate(value)
+ assert checker.to_type(value) is value
+ assert checker.to_json(value) is value
+ assert checker.update(None) is None
+
+def test_sequence_type():
+ """ Test sequence type. """
+
+ # With default sequence builder.
+ checker = parsing.SequenceType(int)
+ checker.validate((1, 2, 3))
+ checker.validate([1, 2, 3])
+ checker.validate({1, 2, 3})
+ checker.validate(SortedSet(int))
+ checker.validate(SortedSet(int, (1, 2, 3)))
+ assert_raises(lambda: checker.validate((1, 2, 3.0)), exceptions.TypeException)
+ assert_raises(lambda: checker.validate((1.0, 2.0, 3.0)), exceptions.TypeException)
+ assert isinstance(checker.to_type((1, 2, 3)), list)
+ # With SortedSet as sequence builder.
+ checker = parsing.SequenceType(float)
+ checker.validate((1.0, 2.0, 3.0))
+ checker.validate([1.0, 2.0, 3.0])
+ checker.validate({1.0, 2.0, 3.0})
+ assert_raises(lambda: checker.validate((1, 2, 3.0)), exceptions.TypeException)
+ assert_raises(lambda: checker.validate((1.0, 2.0, 3)), exceptions.TypeException)
+ checker = parsing.SequenceType(int, sequence_builder=SortedSet.builder(int))
+ initial_list = (1, 2, 7, 7, 1)
+ checker.validate(initial_list)
+ updated_list = checker.update(initial_list)
+ assert isinstance(updated_list, SortedSet) and updated_list.element_type is int
+ assert updated_list == SortedSet(int, (1, 2, 7))
+ assert checker.to_json(updated_list) == [1, 2, 7]
+ assert checker.to_type([7, 2, 1, 1, 7, 1, 7]) == updated_list
+
+def test_jsonable_class_type():
+ """ Test parser for Jsonable sub-classes. """
+
+ checker = parsing.JsonableClassType(MyJsonable)
+ my_jsonable = MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5])
+ my_jsonable_dict = {
+ 'field_a': False,
+ 'field_b': 'test',
+ 'field_e': (1, 2),
+ 'field_f': (1.0, 2.0),
+ }
+ checker.validate(my_jsonable)
+ assert_raises(lambda: checker.validate(None), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(my_jsonable_dict), exceptions.TypeException)
+
+def test_stringable_type():
+ """ Test stringable type. """
+
+ checker = parsing.StringableType(str)
+ checker.validate('0')
+ checker = parsing.StringableType(MyStringable)
+ checker.validate(MyStringable('test'))
+ assert_raises(lambda: checker.validate('test'), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(None), exceptions.TypeException)
+
+def test_dict_type():
+ """ Test dict type. """
+
+ checker = parsing.DictType(str, int)
+ checker.validate({'test': 1})
+ assert_raises(lambda: checker.validate({'test': 1.0}), exceptions.TypeException)
+ checker = parsing.DictType(MyStringable, float)
+ checker.validate({MyStringable('12'): 2.5})
+ assert_raises(lambda: checker.validate({'12': 2.5}), exceptions.TypeException)
+ assert_raises(lambda: checker.validate({MyStringable('12'): 2}), exceptions.TypeException)
+ checker = parsing.DictType(MyStringable, float, dict_builder=SortedDict.builder(MyStringable, float))
+ value = {MyStringable(12): 22.0}
+ checker.validate(value)
+ updated_value = checker.update(value)
+ assert isinstance(updated_value, SortedDict)
+ assert updated_value.key_type is MyStringable
+ assert updated_value.val_type is float
+ json_value = {'MyStringable 12': 22.0}
+ assert checker.to_type(json_value) == SortedDict(MyStringable, float, {MyStringable('12'): 22.0})
+ assert checker.to_json(SortedDict(MyStringable, float, {MyStringable(12): 22.0})) == json_value
+
+def test_sequence_of_values_type():
+ """ Test parser for sequence of allowed values. """
+
+ checker = parsing.EnumerationType({'a', 'b', 'c', 'd'})
+ checker.validate('d')
+ checker.validate('c')
+ checker.validate('b')
+ checker.validate('a')
+ assert_raises(lambda: checker.validate('e'), exceptions.ValueException)
+
+def test_sequence_of_primitives_type():
+ """ Test parser for sequence of primitive types. """
+
+ checker = parsing.SequenceOfPrimitivesType((int, bool))
+ checker.validate(False)
+ checker.validate(True)
+ checker.validate(0)
+ checker.validate(1)
+ assert_raises(lambda: checker.validate(0.0), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(1.0), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(''), exceptions.TypeException)
+ assert_raises(lambda: checker.validate('a non-empty string'), exceptions.TypeException)
+
+def test_primitive_type():
+ """ Test parser for primitive type. """
+
+ checker = parsing.PrimitiveType(bool)
+ checker.validate(True)
+ checker.validate(False)
+ assert_raises(lambda: checker.validate(None), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(0), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(1), exceptions.TypeException)
+ assert_raises(lambda: checker.validate(''), exceptions.TypeException)
+ assert_raises(lambda: checker.validate('a non-empty string'), exceptions.TypeException)
+
+def test_model_parsing():
+ """ Test parsing for a real model. """
+
+ model = {
+ 'name': str,
+ 'language': ('fr', 'en'),
+ 'myjsonable': parsing.JsonableClassType(MyJsonable),
+ 'mydict': parsing.DictType(str, float),
+ 'nothing': (bool, str),
+ 'default_float': parsing.DefaultValueType(float, 33.44),
+ 'optional_float': parsing.OptionalValueType(float)
+ }
+ bad_data_field = {
+ '_name_': 'hello',
+ 'language': 'fr',
+ 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]),
+ 'mydict': {
+ 'a': 2.5,
+ 'b': -1.6
+ },
+ 'nothing': 'thanks'
+ }
+ bad_data_type = {
+ 'name': 'hello',
+ 'language': 'fr',
+ 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]),
+ 'mydict': {
+ 'a': 2.5,
+ 'b': -1.6
+ },
+ 'nothing': 2.5
+ }
+ bad_data_value = {
+ 'name': 'hello',
+ 'language': '__',
+ 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]),
+ 'mydict': {
+ 'a': 2.5,
+ 'b': -1.6
+ },
+ 'nothing': '2.5'
+ }
+ good_data = {
+ 'name': 'hello',
+ 'language': 'fr',
+ 'myjsonable': MyJsonable(field_a=False, field_b='test', field_e={1}, field_f=[6.5]),
+ 'mydict': {
+ 'a': 2.5,
+ 'b': -1.6
+ },
+ 'nothing': '2.5'
+ }
+ assert_raises(lambda: parsing.validate_data(bad_data_field, model), (exceptions.TypeException,))
+ assert_raises(lambda: parsing.validate_data(bad_data_type, model), (exceptions.TypeException,))
+ assert_raises(lambda: parsing.validate_data(bad_data_value, model), (exceptions.ValueException,))
+
+ assert 'default_float' not in good_data
+ assert 'optional_float' not in good_data
+ parsing.validate_data(good_data, model)
+ updated_good_data = parsing.update_data(good_data, model)
+ assert 'default_float' in updated_good_data
+ assert 'optional_float' in updated_good_data
+ assert updated_good_data['default_float'] == 33.44
+ assert updated_good_data['optional_float'] is None
+
+def test_converter_type():
+ """ Test parser converter type. """
+
+ def converter_to_int(val):
+ """ Converts value to integer """
+ try:
+ return int(val)
+ except (ValueError, TypeError):
+ return 0
+
+ checker = parsing.ConverterType(str, converter_function=lambda val: 'String of %s' % val)
+ checker.validate('a string')
+ checker.validate(10)
+ checker.validate(True)
+ checker.validate(None)
+ checker.validate(-2.5)
+ assert checker.update(10) == 'String of 10'
+ assert checker.update(False) == 'String of False'
+ assert checker.update('string') == 'String of string'
+ checker = parsing.ConverterType(int, converter_function=converter_to_int)
+ checker.validate(10)
+ checker.validate(True)
+ checker.validate(None)
+ checker.validate(-2.5)
+ assert checker.update(10) == 10
+ assert checker.update(True) == 1
+ assert checker.update(False) == 0
+ assert checker.update(-2.5) == -2
+ assert checker.update('44') == 44
+ assert checker.update('a') == 0
+
+def test_indexed_sequence():
+ """ Test parser type for dicts stored as sequences. """
+ checker = parsing.IndexedSequenceType(parsing.DictType(str, parsing.JsonableClassType(MyJsonable)), 'field_b')
+ sequence = [
+ MyJsonable(field_a=True, field_b='x1', field_e=[1, 2, 3], field_f=[1., 2., 3.]),
+ MyJsonable(field_a=True, field_b='x3', field_e=[1, 2, 3], field_f=[1., 2., 3.]),
+ MyJsonable(field_a=True, field_b='x2', field_e=[1, 2, 3], field_f=[1., 2., 3.]),
+ MyJsonable(field_a=True, field_b='x5', field_e=[1, 2, 3], field_f=[1., 2., 3.]),
+ MyJsonable(field_a=True, field_b='x4', field_e=[1, 2, 3], field_f=[1., 2., 3.])
+ ]
+ dct = {element.field_b: element for element in sequence}
+ checker.validate(dct)
+ checker.update(dct)
+ jval = checker.to_json(dct)
+ assert isinstance(jval, list), type(jval)
+ from_jval = checker.to_type(jval)
+ assert isinstance(from_jval, dict), type(from_jval)
+ assert len(dct) == 5
+ assert len(from_jval) == 5
+ for key in ('x1', 'x2', 'x3', 'x4', 'x5'):
+ assert key in dct, (key, list(dct.keys()))
+ assert key in from_jval, (key, list(from_jval.keys()))
diff --git a/diplomacy/utils/tests/test_priority_dict.py b/diplomacy/utils/tests/test_priority_dict.py
new file mode 100644
index 0000000..cb7023d
--- /dev/null
+++ b/diplomacy/utils/tests/test_priority_dict.py
@@ -0,0 +1,102 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test class PriorityDict. """
+from diplomacy.utils.priority_dict import PriorityDict
+from diplomacy.utils.tests.test_common import assert_equals
+
+def test_priority_dict():
+ """ Test Heap class PriorityDict. """
+
+ for unordered_list in [
+ [464, 21, 43453, 211, 324, 321, 102, 1211, 14, 875, 1, 33444, 22],
+ 'once upon a time in West'.split(),
+ 'This is a sentence with many words like panthera, lion, tiger, cat or cheetah!'.split()
+ ]:
+ expected_ordered_set = list(sorted(set(unordered_list)))
+ computed_sorted_list = []
+ priority_dict = PriorityDict()
+ for element in unordered_list:
+ priority_dict[element] = element
+ while priority_dict:
+ value, key = priority_dict.smallest()
+ computed_sorted_list.append(value)
+ del priority_dict[key]
+ assert_equals(expected_ordered_set, computed_sorted_list)
+
+def test_item_getter_setter_deletion():
+ """ Test PriorityDict item setter/getter/deletion. """
+
+ priority_dict = PriorityDict()
+ priority_dict['a'] = 12
+ priority_dict['f'] = 9
+ priority_dict['b'] = 23
+ assert list(priority_dict.keys()) == ['f', 'a', 'b']
+ assert priority_dict['a'] == 12
+ assert priority_dict['f'] == 9
+ assert priority_dict['b'] == 23
+ priority_dict['e'] = -1
+ priority_dict['a'] = 8
+ del priority_dict['b']
+ assert list(priority_dict.keys()) == ['e', 'a', 'f']
+ assert list(priority_dict.values()) == [-1, 8, 9]
+
+def test_iterations():
+ """ test iterations:
+ - for key in priority_dict
+ - priority_dict.keys()
+ - priority_dict.values()
+ - priority_dict.items()
+ """
+ priorities = [464, 21, 43453, 211, 324, 321, 102, 1211, 14, 875, 1, 33444, 22]
+
+ # Build priority dict.
+ priority_dict = PriorityDict()
+ for priority in priorities:
+ priority_dict['value of %s' % priority] = priority
+
+ # Build expected priorities and keys.
+ expected_sorted_priorities = list(sorted(priorities))
+ expected_sorted_keys = ['value of %s' % priority for priority in sorted(priorities)]
+
+ # Iterate on priority dict.
+ computed_sorted_priorities = [priority_dict[key] for key in priority_dict]
+ # Iterate on priority dict keys.
+ sorted_priorities_from_key = [priority_dict[key] for key in priority_dict.keys()]
+ # Iterate on priority dict values.
+ sorted_priorities_from_values = list(priority_dict.values())
+ # Iterate on priority dict items.
+ priority_dict_items = list(priority_dict.items())
+ # Get priority dict keys.
+ priority_dict_keys = list(priority_dict.keys())
+ # Get priority dict keys from items (to validate items).
+ priority_dict_keys_from_items = [item[0] for item in priority_dict_items]
+ # Get priority dict values from items (to validate items).
+ priority_dict_values_from_items = [item[1] for item in priority_dict_items]
+
+ for expected, computed in [
+ (expected_sorted_priorities, computed_sorted_priorities),
+ (expected_sorted_priorities, sorted_priorities_from_key),
+ (expected_sorted_priorities, sorted_priorities_from_values),
+ (expected_sorted_priorities, priority_dict_values_from_items),
+ (expected_sorted_keys, priority_dict_keys_from_items),
+ (expected_sorted_keys, priority_dict_keys),
+ ]:
+ assert_equals(expected, computed)
+
+ # Priority dict should have not been modified.
+ assert_equals(len(priorities), len(priority_dict))
+ assert all(key in priority_dict for key in expected_sorted_keys)
diff --git a/diplomacy/utils/tests/test_sorted_dict.py b/diplomacy/utils/tests/test_sorted_dict.py
new file mode 100644
index 0000000..559c36d
--- /dev/null
+++ b/diplomacy/utils/tests/test_sorted_dict.py
@@ -0,0 +1,154 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test class SortedDict. """
+from diplomacy.utils import common
+from diplomacy.utils.sorted_dict import SortedDict
+from diplomacy.utils.tests.test_common import assert_equals
+
+def test_init_bool_and_len():
+ """ Test SortedDict initialization, length and conversion to boolean. """
+
+ sorted_dict = SortedDict(int, str)
+ assert not sorted_dict
+ sorted_dict = SortedDict(int, str, {2: 'two', 4: 'four', 99: 'ninety-nine'})
+ assert sorted_dict
+ assert len(sorted_dict) == 3
+
+def test_builder_and_properties():
+ """ Test SortedDict builder and properties key_type and val_type. """
+
+ builder_float_to_bool = SortedDict.builder(float, bool)
+ sorted_dict = builder_float_to_bool({2.5: True, 2.7: False, 2.9: True})
+ assert isinstance(sorted_dict, SortedDict) and sorted_dict.key_type is float and sorted_dict.val_type is bool
+
+def test_items_functions():
+ """ Test SortedDict item setter/getter and methods put() and __contains__(). """
+
+ expected_keys = ['cat', 'lion', 'panthera', 'serval', 'tiger']
+ sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7})
+ # Test setter.
+ sorted_dict['panthera'] = -.88
+ sorted_dict['cat'] = 2223.
+ # Test put().
+ sorted_dict.put('serval', 39e12)
+ # Test __contains__.
+ assert 'lions' not in sorted_dict
+ assert all(key in sorted_dict for key in expected_keys)
+ # Test getter.
+ assert sorted_dict['cat'] == 2223.
+ assert sorted_dict['serval'] == 39e12
+ # Test setter then getter.
+ assert sorted_dict['lion'] == 1.5
+ sorted_dict['lion'] = 2.3
+ assert sorted_dict['lion'] == 2.3
+ # Test get,
+ assert sorted_dict.get('lions') is None
+ assert sorted_dict.get('lion') == 2.3
+
+def test_item_deletion_and_remove():
+ """ Test SortedDict methods remove() and __delitem__(). """
+
+ sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7, 'panthera': -.88, 'cat': 2223., 'serval': 39e12})
+ assert len(sorted_dict) == 5
+ assert 'serval' in sorted_dict
+ sorted_dict.remove('serval')
+ assert len(sorted_dict) == 4
+ assert 'serval' not in sorted_dict
+ removed = sorted_dict.remove('tiger')
+ assert len(sorted_dict) == 3
+ assert 'tiger' not in sorted_dict
+ assert removed == -2.7
+ assert sorted_dict.remove('tiger') is None
+ assert sorted_dict.remove('key not in dict') is None
+ del sorted_dict['panthera']
+ assert len(sorted_dict) == 2
+ assert 'panthera' not in sorted_dict
+ assert 'cat' in sorted_dict
+ assert 'lion' in sorted_dict
+
+def test_iterations():
+ """ Test SortedDict iterations (for key in dict, keys(), values(), items()). """
+
+ expected_sorted_keys = ['cat', 'lion', 'panthera', 'serval', 'tiger']
+ expected_sorted_values = [2223., 1.5, -.88, 39e12, -2.7]
+ sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7, 'panthera': -.88, 'cat': 2223., 'serval': 39e12})
+ computed_sorted_keys = [key for key in sorted_dict]
+ computed_sorted_keys_from_keys = list(sorted_dict.keys())
+ computed_sorted_values = list(sorted_dict.values())
+ keys_from_items = []
+ values_from_items = []
+ for key, value in sorted_dict.items():
+ keys_from_items.append(key)
+ values_from_items.append(value)
+ assert_equals(expected_sorted_keys, computed_sorted_keys)
+ assert_equals(expected_sorted_keys, computed_sorted_keys_from_keys)
+ assert_equals(expected_sorted_keys, keys_from_items)
+ assert_equals(expected_sorted_values, values_from_items)
+ assert_equals(expected_sorted_values, computed_sorted_values)
+
+def test_bound_keys_getters():
+ """ Test SortedDict methods first_key(), last_key(), last_value(), last_item(),
+ get_previous_key(), get_next_key().
+ """
+
+ sorted_dict = SortedDict(str, float, {'lion': 1.5, 'tiger': -2.7})
+ sorted_dict['panthera'] = -.88
+ sorted_dict['cat'] = 2223.
+ sorted_dict['serval'] = 39e12
+ assert sorted_dict.first_key() == 'cat'
+ assert sorted_dict.last_key() == 'tiger'
+ assert sorted_dict.last_value() == sorted_dict['tiger'] == -2.7
+ assert sorted_dict.last_item() == ('tiger', -2.7)
+ assert sorted_dict.get_previous_key('cat') is None
+ assert sorted_dict.get_next_key('cat') == 'lion'
+ assert sorted_dict.get_previous_key('tiger') == 'serval'
+ assert sorted_dict.get_next_key('tiger') is None
+ assert sorted_dict.get_previous_key('panthera') == 'lion'
+ assert sorted_dict.get_next_key('panthera') == 'serval'
+
+def test_equality():
+ """ Test SortedDict equality. """
+
+ empty_sorted_dict_float_int = SortedDict(float, int)
+ empty_sorted_dict_float_bool_1 = SortedDict(float, bool)
+ empty_sorted_dict_float_bool_2 = SortedDict(float, bool)
+ sorted_dict_float_int_1 = SortedDict(float, int, {2.5: 17, 3.3: 49, -5.7: 71})
+ sorted_dict_float_int_2 = SortedDict(float, int, {2.5: 17, 3.3: 49, -5.7: 71})
+ sorted_dict_float_int_3 = SortedDict(float, int, {2.5: -17, 3.3: 49, -5.7: 71})
+ assert empty_sorted_dict_float_int != empty_sorted_dict_float_bool_1
+ assert empty_sorted_dict_float_bool_1 == empty_sorted_dict_float_bool_2
+ assert sorted_dict_float_int_1 == sorted_dict_float_int_2
+ assert sorted_dict_float_int_1 != sorted_dict_float_int_3
+
+def test_sub_and_remove_sub():
+ """Test SortedDict methods sub() and remove_sub()."""
+
+ sorted_dict = SortedDict(int, str, {k: 'value of %s' % k for k in (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4)})
+ assert sorted_dict.sub() == list(sorted_dict.values())
+ assert sorted_dict.sub(-10, 4) == ['value of 0', 'value of 1', 'value of 2', 'value of 3', 'value of 4']
+ assert sorted_dict.sub(15) == ['value of 17', 'value of 20']
+ sorted_dict.remove_sub(-10, 4)
+ assert all(k not in sorted_dict for k in (0, 1, 2, 3, 4))
+ sorted_dict.remove_sub(15)
+ assert all(k not in sorted_dict for k in (17, 20))
+
+def test_is_sequence_and_is_dict():
+ """Check sorted dict with is_sequence() and is_dict()."""
+
+ assert common.is_dictionary(SortedDict(str, int, {'a': 3, 'b': -1, 'c': 12}))
+ assert common.is_dictionary(SortedDict(int, float), )
+ assert not common.is_sequence(SortedDict(str, str))
diff --git a/diplomacy/utils/tests/test_sorted_set.py b/diplomacy/utils/tests/test_sorted_set.py
new file mode 100644
index 0000000..1208cd3
--- /dev/null
+++ b/diplomacy/utils/tests/test_sorted_set.py
@@ -0,0 +1,168 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Test class SortedSet. """
+from diplomacy.utils import common
+from diplomacy.utils.sorted_set import SortedSet
+from diplomacy.utils.tests.test_common import assert_equals
+
+def test_init_bool_and_len():
+ """ Test SortedSet initialization, length and conversion to boolean. """
+
+ sorted_set = SortedSet(int)
+ assert not sorted_set
+ sorted_set = SortedSet(int, (2, 4, 99))
+ assert sorted_set
+ assert len(sorted_set) == 3
+
+def test_builder_and_property():
+ """ Test SortedSet builder and property element_type. """
+
+ builder_float = SortedSet.builder(float)
+ sorted_set = builder_float((2.5, 2.7, 2.9))
+ assert isinstance(sorted_set, SortedSet) and sorted_set.element_type is float
+
+def test_item_add_get_and_contains():
+ """ Test SortedSet methods add(), __getitem__(), and __contains__(). """
+
+ expected_values = ['cat', 'lion', 'panthera', 'serval', 'tiger']
+ sorted_set = SortedSet(str, ('lion', 'tiger'))
+ # Test setter.
+ sorted_set.add('panthera')
+ sorted_set.add('cat')
+ sorted_set.add('serval')
+ # Test __contains__.
+ assert 'lions' not in sorted_set
+ assert all(key in sorted_set for key in expected_values)
+ # Test getter.
+ assert sorted_set[0] == 'cat'
+ assert sorted_set[1] == 'lion'
+ assert sorted_set[2] == 'panthera'
+ assert sorted_set[3] == 'serval'
+ assert sorted_set[4] == 'tiger'
+ # Test add then getter.
+ sorted_set.add('onca')
+ assert sorted_set[1] == 'lion'
+ assert sorted_set[2] == 'onca'
+ assert sorted_set[3] == 'panthera'
+
+def test_pop_and_remove():
+ """ Test SortedSet methods remove() and pop(). """
+
+ sorted_set = SortedSet(str, ('lion', 'tiger', 'panthera', 'cat', 'serval'))
+ assert len(sorted_set) == 5
+ assert 'serval' in sorted_set
+ sorted_set.remove('serval')
+ assert len(sorted_set) == 4
+ assert 'serval' not in sorted_set
+ assert sorted_set.remove('tiger') == 'tiger'
+ assert len(sorted_set) == 3
+ assert 'tiger' not in sorted_set
+ assert sorted_set.remove('tiger') is None
+ assert sorted_set.remove('key not in set') is None
+ index_of_panthera = sorted_set.index('panthera')
+ assert index_of_panthera == 2
+ assert sorted_set.pop(index_of_panthera) == 'panthera'
+ assert len(sorted_set) == 2
+ assert 'panthera' not in sorted_set
+ assert 'cat' in sorted_set
+ assert 'lion' in sorted_set
+
+def test_iteration():
+ """ Test SortedSet iteration. """
+
+ expected_sorted_values = ['cat', 'lion', 'panthera', 'serval', 'tiger']
+ sorted_set = SortedSet(str, ('lion', 'tiger', 'panthera', 'cat', 'serval'))
+ computed_sorted_values = [key for key in sorted_set]
+ assert_equals(expected_sorted_values, computed_sorted_values)
+
+def test_equality():
+ """ Test SortedSet equality. """
+
+ empty_sorted_set_float = SortedSet(float)
+ empty_sorted_set_int = SortedSet(int)
+ another_empty_sorted_set_int = SortedSet(int)
+ sorted_set_float_1 = SortedSet(float, (2.5, 3.3, -5.7))
+ sorted_set_float_2 = SortedSet(float, (2.5, 3.3, -5.7))
+ sorted_set_float_3 = SortedSet(float, (2.5, 3.3, 5.7))
+ assert empty_sorted_set_float != empty_sorted_set_int
+ assert empty_sorted_set_int == another_empty_sorted_set_int
+ assert sorted_set_float_1 == sorted_set_float_2
+ assert sorted_set_float_1 != sorted_set_float_3
+
+def test_getters_around_values():
+ """Test SortedSet methods get_next_value() and get_previous_value()."""
+
+ sorted_set = SortedSet(int, (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4))
+ expected = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 20)
+ assert sorted_set
+ assert len(sorted_set) == len(expected)
+ assert all(expected[i] == sorted_set[i] for i in range(len(expected)))
+ assert all(e in sorted_set for e in expected)
+ assert sorted_set.get_next_value(0) == 1
+ assert sorted_set.get_next_value(5) == 6
+ assert sorted_set.get_next_value(9) == 17
+ assert sorted_set.get_next_value(-1) == 0
+ assert sorted_set.get_next_value(20) is None
+ assert sorted_set.get_previous_value(0) is None
+ assert sorted_set.get_previous_value(17) == 9
+ assert sorted_set.get_previous_value(20) == 17
+ assert sorted_set.get_previous_value(1) == 0
+ assert sorted_set.get_previous_value(6) == 5
+
+ assert sorted_set.get_next_value(3) == 4
+ assert sorted_set.get_next_value(4) == 5
+ assert sorted_set.get_next_value(7) == 8
+ assert sorted_set.get_next_value(8) == 9
+ assert sorted_set.get_previous_value(5) == 4
+ assert sorted_set.get_previous_value(4) == 3
+ assert sorted_set.get_previous_value(9) == 8
+ assert sorted_set.get_previous_value(8) == 7
+ sorted_set.remove(8)
+ assert len(sorted_set) == len(expected) - 1
+ assert 8 not in sorted_set
+ sorted_set.remove(4)
+ assert len(sorted_set) == len(expected) - 2
+ assert 4 not in sorted_set
+ assert sorted_set.get_next_value(3) == 5
+ assert sorted_set.get_next_value(4) == 5
+ assert sorted_set.get_next_value(7) == 9
+ assert sorted_set.get_next_value(8) == 9
+ assert sorted_set.get_previous_value(5) == 3
+ assert sorted_set.get_previous_value(4) == 3
+ assert sorted_set.get_previous_value(9) == 7
+ assert sorted_set.get_previous_value(8) == 7
+
+def test_index():
+ """ Test SortedSet method index(). """
+
+ sorted_set = SortedSet(int, (2, 5, 1, 9, 4, 5, 20, 0, 6, 17, 8, 3, 7, 0, 4))
+ sorted_set.remove(8)
+ sorted_set.remove(4)
+ index_of_2 = sorted_set.index(2)
+ index_of_17 = sorted_set.index(17)
+ assert index_of_2 == 2
+ assert sorted_set.index(4) is None
+ assert sorted_set.index(8) is None
+ assert index_of_17 == len(sorted_set) - 2
+ assert sorted_set.pop(index_of_2) == 2
+
+def test_common_utils_with_sorted_set():
+ """Check sorted set with is_sequence() and is_dictionary()."""
+ assert common.is_sequence(SortedSet(int, (1, 2, 3)))
+ assert common.is_sequence(SortedSet(int))
+ assert not common.is_dictionary(SortedSet(int, (1, 2, 3)))
+ assert not common.is_dictionary(SortedSet(int))
diff --git a/diplomacy/utils/tests/test_time.py b/diplomacy/utils/tests/test_time.py
new file mode 100644
index 0000000..a2e7a63
--- /dev/null
+++ b/diplomacy/utils/tests/test_time.py
@@ -0,0 +1,77 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Tests cases for time function"""
+from diplomacy.utils import str_to_seconds, next_time_at, trunc_time
+
+def test_str_to_seconds():
+ """ Tests for str_to_seconds """
+ assert str_to_seconds('1W') == 604800
+ assert str_to_seconds('1D') == 86400
+ assert str_to_seconds('1H') == 3600
+ assert str_to_seconds('1M') == 60
+ assert str_to_seconds('1S') == 1
+ assert str_to_seconds('1') == 1
+ assert str_to_seconds(1) == 1
+
+ assert str_to_seconds('10W') == 10 * 604800
+ assert str_to_seconds('10D') == 10 * 86400
+ assert str_to_seconds('10H') == 10 * 3600
+ assert str_to_seconds('10M') == 10 * 60
+ assert str_to_seconds('10S') == 10 * 1
+ assert str_to_seconds('10') == 10 * 1
+ assert str_to_seconds(10) == 10 * 1
+
+ assert str_to_seconds('1W2D3H4M5S') == 1 * 604800 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5
+ assert str_to_seconds('1W2D3H4M5') == 1 * 604800 + 2 * 86400 + 3 * 3600 + 4 * 60 + 5
+ assert str_to_seconds('11W12D13H14M15S') == 11 * 604800 + 12 * 86400 + 13 * 3600 + 14 * 60 + 15
+ assert str_to_seconds('11W12D13H14M15') == 11 * 604800 + 12 * 86400 + 13 * 3600 + 14 * 60 + 15
+
+def test_trunc_time():
+ """ Tests for trunc_time """
+ # 1498746123 = Thursday, June 29, 2017 10:22:03 AM GMT-04:00 DST
+ assert trunc_time(1498746123, '1M', 'America/Montreal') == 1498746180 # 10:23
+ assert trunc_time(1498746123, '5M', 'America/Montreal') == 1498746300 # 10:25
+ assert trunc_time(1498746123, '10M', 'America/Montreal') == 1498746600 # 10:30
+ assert trunc_time(1498746123, '15M', 'America/Montreal') == 1498746600 # 10:30
+ assert trunc_time(1498746123, '20M', 'America/Montreal') == 1498747200 # 10:40
+ assert trunc_time(1498746123, '25M', 'America/Montreal') == 1498746300 # 10:25
+
+ # 1498731723 = Thursday, June 29, 2017 10:22:03 AM GMT
+ assert trunc_time(1498731723, '1M', 'GMT') == 1498731780 # 10:23
+ assert trunc_time(1498731723, '5M', 'GMT') == 1498731900 # 10:25
+ assert trunc_time(1498731723, '10M', 'GMT') == 1498732200 # 10:30
+ assert trunc_time(1498731723, '15M', 'GMT') == 1498732200 # 10:30
+ assert trunc_time(1498731723, '20M', 'GMT') == 1498732800 # 10:40
+ assert trunc_time(1498731723, '25M', 'GMT') == 1498731900 # 10:25
+
+def test_next_time_at():
+ """ Tests for next_time_at """
+ # 1498746123 = Thursday, June 29, 2017 10:22:03 AM GMT-04:00 DST
+ assert next_time_at(1498746123, '10:23', 'America/Montreal') == 1498746180 # 10:23
+ assert next_time_at(1498746123, '10:25', 'America/Montreal') == 1498746300 # 10:25
+ assert next_time_at(1498746123, '10:30', 'America/Montreal') == 1498746600 # 10:30
+ assert next_time_at(1498746123, '10:40', 'America/Montreal') == 1498747200 # 10:40
+ assert next_time_at(1498746123, '16:40', 'America/Montreal') == 1498768800 # 16:40
+ assert next_time_at(1498746123, '6:20', 'America/Montreal') == 1498818000 # 6:20 (Next day)
+
+ # 1498731723 = Thursday, June 29, 2017 10:22:03 AM GMT
+ assert next_time_at(1498731723, '10:23', 'GMT') == 1498731780 # 10:23
+ assert next_time_at(1498731723, '10:25', 'GMT') == 1498731900 # 10:25
+ assert next_time_at(1498731723, '10:30', 'GMT') == 1498732200 # 10:30
+ assert next_time_at(1498731723, '10:40', 'GMT') == 1498732800 # 10:40
+ assert next_time_at(1498731723, '16:40', 'GMT') == 1498754400 # 16:40
+ assert next_time_at(1498731723, '6:20', 'GMT') == 1498803600 # 6:20 (Next day)