assertman2 0.2.29__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assertman2-0.2.29/PKG-INFO +13 -0
- assertman2-0.2.29/assertman/__init__.py +2 -0
- assertman2-0.2.29/assertman/assert_that.py +10 -0
- assertman2-0.2.29/assertman/assertable_mixin.py +36 -0
- assertman2-0.2.29/assertman/cerberus_wrapper/__init__.py +1 -0
- assertman2-0.2.29/assertman/cerberus_wrapper/assert_that.py +97 -0
- assertman2-0.2.29/assertman/cerberus_wrapper/mapping.py +420 -0
- assertman2-0.2.29/assertman/cerberus_wrapper/pretty_error.py +38 -0
- assertman2-0.2.29/assertman/cerberus_wrapper/validators.py +413 -0
- assertman2-0.2.29/assertman/helpers/__init__.py +0 -0
- assertman2-0.2.29/assertman/helpers/datetime_helper.py +274 -0
- assertman2-0.2.29/assertman/helpers/jsonpath_helper.py +29 -0
- assertman2-0.2.29/assertman/helpers/waiting_helper.py +92 -0
- assertman2-0.2.29/assertman/matchers/__init__.py +7 -0
- assertman2-0.2.29/assertman/matchers/datetimes.py +300 -0
- assertman2-0.2.29/assertman/matchers/dictionary.py +5 -0
- assertman2-0.2.29/assertman/matchers/logical.py +6 -0
- assertman2-0.2.29/assertman/matchers/numbers.py +8 -0
- assertman2-0.2.29/assertman/matchers/object.py +11 -0
- assertman2-0.2.29/assertman/matchers/sequence.py +194 -0
- assertman2-0.2.29/assertman/matchers/text.py +10 -0
- assertman2-0.2.29/assertman/objects/__init__.py +32 -0
- assertman2-0.2.29/assertman/objects/assertable_api_response.py +65 -0
- assertman2-0.2.29/assertman/objects/assertable_bool.py +21 -0
- assertman2-0.2.29/assertman/objects/assertable_datetime.py +8 -0
- assertman2-0.2.29/assertman/objects/assertable_dict.py +43 -0
- assertman2-0.2.29/assertman/objects/assertable_float.py +10 -0
- assertman2-0.2.29/assertman/objects/assertable_int.py +10 -0
- assertman2-0.2.29/assertman/objects/assertable_list.py +88 -0
- assertman2-0.2.29/assertman/objects/assertable_none.py +21 -0
- assertman2-0.2.29/assertman/objects/assertable_string.py +9 -0
- assertman2-0.2.29/assertman2.egg-info/PKG-INFO +13 -0
- assertman2-0.2.29/assertman2.egg-info/SOURCES.txt +65 -0
- assertman2-0.2.29/assertman2.egg-info/dependency_links.txt +1 -0
- assertman2-0.2.29/assertman2.egg-info/requires.txt +5 -0
- assertman2-0.2.29/assertman2.egg-info/top_level.txt +3 -0
- assertman2-0.2.29/docs/__init__.py +0 -0
- assertman2-0.2.29/setup.cfg +4 -0
- assertman2-0.2.29/setup.py +25 -0
- assertman2-0.2.29/tests/__init__.py +0 -0
- assertman2-0.2.29/tests/conftest.py +61 -0
- assertman2-0.2.29/tests/extra/__init__.py +0 -0
- assertman2-0.2.29/tests/extra/dict/__init__.py +0 -0
- assertman2-0.2.29/tests/extra/dict/test_dict_extract.py +41 -0
- assertman2-0.2.29/tests/extra/dict/test_jsonpatch.py +105 -0
- assertman2-0.2.29/tests/extra/list/__init__.py +0 -0
- assertman2-0.2.29/tests/extra/list/test_list_filter.py +129 -0
- assertman2-0.2.29/tests/extra/list/test_list_find.py +107 -0
- assertman2-0.2.29/tests/extra/list/test_list_get_by_index.py +21 -0
- assertman2-0.2.29/tests/extra/response/__init__.py +0 -0
- assertman2-0.2.29/tests/extra/response/test_request.py +86 -0
- assertman2-0.2.29/tests/new/__init__.py +0 -0
- assertman2-0.2.29/tests/new/pretty.py +22 -0
- assertman2-0.2.29/tests/new/prettylist.py +9 -0
- assertman2-0.2.29/tests/objects/__init__.py +0 -0
- assertman2-0.2.29/tests/objects/test_1_integer.py +27 -0
- assertman2-0.2.29/tests/objects/test_2_string.py +25 -0
- assertman2-0.2.29/tests/objects/test_3_1_list.py +107 -0
- assertman2-0.2.29/tests/objects/test_3_2_list_with_dicts.py +94 -0
- assertman2-0.2.29/tests/objects/test_4_1_dict.py +50 -0
- assertman2-0.2.29/tests/objects/test_4_2_dict_with_lists_has_length.py +33 -0
- assertman2-0.2.29/tests/objects/test_4_3_dict_with_lists_has_item.py +217 -0
- assertman2-0.2.29/tests/objects/test_4_4_dict_has_key.py +47 -0
- assertman2-0.2.29/tests/objects/test_4_5_dict_with_lists_every_item.py +0 -0
- assertman2-0.2.29/tests/objects/test_5_datetime.py +340 -0
- assertman2-0.2.29/tests/temp_test_validate_schema.py +47 -0
- assertman2-0.2.29/tests/utils.py +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 1.1
|
|
2
|
+
Name: assertman2
|
|
3
|
+
Version: 0.2.29
|
|
4
|
+
Summary: UNKNOWN
|
|
5
|
+
Home-page: UNKNOWN
|
|
6
|
+
Author: UNKNOWN
|
|
7
|
+
Author-email: UNKNOWN
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Description: UNKNOWN
|
|
10
|
+
Platform: UNKNOWN
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import hamcrest
|
|
2
|
+
from hamcrest.core.base_matcher import BaseMatcher
|
|
3
|
+
from assertman import cerberus_wrapper
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__tracebackhide__ = True
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AssertableMixin:
|
|
10
|
+
|
|
11
|
+
_assertion_processing = None
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def _assertable_data(self):
|
|
15
|
+
raise NotImplementedError(
|
|
16
|
+
'Что бы добавить объекту свойство `assertable`, определите в нем метод `_assertable_data`')
|
|
17
|
+
|
|
18
|
+
def should(self, matcher):
|
|
19
|
+
if not isinstance(matcher, BaseMatcher):
|
|
20
|
+
raise TypeError(f'Переданный матчер имеет неподдерживаемый тип {type(matcher)}')
|
|
21
|
+
|
|
22
|
+
if type(matcher).__name__ in ['IsDictContainingEntries']:
|
|
23
|
+
cerberus_wrapper.assert_that(self._assertable_data, matcher)
|
|
24
|
+
else:
|
|
25
|
+
hamcrest.assert_that(self._assertable_data, matcher)
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def filter(self, *args, **kwargs):
|
|
29
|
+
raise NotImplementedError("Фильтрация доступна только для списков")
|
|
30
|
+
|
|
31
|
+
def find(self, *args, **kwargs):
|
|
32
|
+
raise NotImplementedError("Поиск элемента доступен только для списков")
|
|
33
|
+
|
|
34
|
+
def extract(self, query):
|
|
35
|
+
raise NotImplementedError("Извлечение по ключу доступно только для словарей")
|
|
36
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from assertman.cerberus_wrapper.assert_that import assert_that
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from assertman.cerberus_wrapper import mapping
|
|
2
|
+
from assertman.cerberus_wrapper import validators
|
|
3
|
+
from assertman.cerberus_wrapper import pretty_error
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
import io
|
|
6
|
+
from rich.traceback import install
|
|
7
|
+
|
|
8
|
+
install()
|
|
9
|
+
|
|
10
|
+
def assert_that(document, matcher):
|
|
11
|
+
__tracebackhide__ = True
|
|
12
|
+
|
|
13
|
+
# Проверяем, что валидируемый документ - это словарь
|
|
14
|
+
if not isinstance(document, (dict, list)):
|
|
15
|
+
raise TypeError('Валидируемый документ должен быть словарем (json) или list')
|
|
16
|
+
|
|
17
|
+
# Базовая схема
|
|
18
|
+
_base_schema = {}
|
|
19
|
+
|
|
20
|
+
# Конвертируем hamcrest-матчеры в формат cerberus-схемы
|
|
21
|
+
_matchers_schema = mapping.extract(matcher)
|
|
22
|
+
|
|
23
|
+
if _matchers_schema.get('schema') is None:
|
|
24
|
+
_matchers_schema = {'schema': _matchers_schema, 'type': 'dict'}
|
|
25
|
+
|
|
26
|
+
# TODO: (Кузнецов М.) Написать тесты на то что схема дополняется, а не заменяется
|
|
27
|
+
for k, v in _matchers_schema['schema'].items():
|
|
28
|
+
if k not in _base_schema:
|
|
29
|
+
_base_schema.update({k: v})
|
|
30
|
+
else:
|
|
31
|
+
_base_schema[k].update(v)
|
|
32
|
+
|
|
33
|
+
# Выполняем валидацию ответа по схеме
|
|
34
|
+
# (не менять тут схему)
|
|
35
|
+
validator = validators.SkyrimValidator(
|
|
36
|
+
_base_schema,
|
|
37
|
+
allow_unknown=True,
|
|
38
|
+
require_all=True
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
result = validator.validate(document)
|
|
42
|
+
|
|
43
|
+
# Если при проведени валидации по схеме найдены ошибки, выкидываем AssertionError со сформировнным текстом ошибок
|
|
44
|
+
if result is False:
|
|
45
|
+
# Если в результате проверок нашлись ошибки, то выкидываем AssertionError c перечнем ошибок
|
|
46
|
+
error_text = pretty_error.make_assertion_text(validator.errors)
|
|
47
|
+
raise AssertionError("".join(error_text))
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def assert_that2(document, matcher):
|
|
53
|
+
__tracebackhide__ = True
|
|
54
|
+
|
|
55
|
+
# Проверяем, что валидируемый документ - это словарь
|
|
56
|
+
if not isinstance(document, dict):
|
|
57
|
+
raise TypeError('Валидируемый документ должен быть словарем (json)')
|
|
58
|
+
|
|
59
|
+
# Базовая схема
|
|
60
|
+
_base_schema = {}
|
|
61
|
+
|
|
62
|
+
# Конвертируем hamcrest-матчеры в формат cerberus-схемы
|
|
63
|
+
_matchers_schema = mapping.extract(matcher)
|
|
64
|
+
|
|
65
|
+
# TODO: (Кузнецов М.) Написать тесты на то что схема дополняется, а не заменяется
|
|
66
|
+
for k, v in _matchers_schema['schema'].items():
|
|
67
|
+
if k not in _base_schema:
|
|
68
|
+
_base_schema.update({k: v})
|
|
69
|
+
else:
|
|
70
|
+
_base_schema[k].update(v)
|
|
71
|
+
|
|
72
|
+
# Выполняем валидацию ответа по схеме
|
|
73
|
+
# (нее менять тут схему)
|
|
74
|
+
validator = validators.SkyrimValidator(
|
|
75
|
+
_base_schema,
|
|
76
|
+
allow_unknown=True,
|
|
77
|
+
require_all=True
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
result = validator.validate(document)
|
|
81
|
+
|
|
82
|
+
# Если при проведени валидации по схеме найдены ошибки, выкидываем AssertionError со сформировнным текстом ошибок
|
|
83
|
+
if result is False:
|
|
84
|
+
install()
|
|
85
|
+
# Если в результате проверок нашлись ошибки, то выкидываем AssertionError c перечнем ошибок
|
|
86
|
+
error_text = pretty_error.make_assertion_text(validator.errors)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
console = Console(color_system="windows", file=io.StringIO(), width=120)
|
|
90
|
+
|
|
91
|
+
console.print("".join(error_text))
|
|
92
|
+
output = console.file.getvalue()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
raise AssertionError(output)
|
|
96
|
+
|
|
97
|
+
return True
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# ------ Общие матчеры (Object) ------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
def equal_to(obj):
|
|
7
|
+
""" Matches if object is equal to a given object """
|
|
8
|
+
schema = {"equal_to": obj}
|
|
9
|
+
# по-умолчанию cerberus схема не допускает наличие None значений. При проверки на equal_to(None), отключаем
|
|
10
|
+
# это ограничение, только для конкретного поля
|
|
11
|
+
if obj is None:
|
|
12
|
+
schema.update({"nullable": True})
|
|
13
|
+
return schema
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_length(match):
|
|
17
|
+
""" Matches if ``len(item)`` satisfies a given matcher
|
|
18
|
+
* матчер проверяет длинну любого sequence-типа данных: list, str, dict, tuple и кастомнный тип данных с
|
|
19
|
+
переопределенным методом `__len__`
|
|
20
|
+
"""
|
|
21
|
+
# TODO (Кузнецов М.): Дописать проверкау то что items должен быть числом или матчером
|
|
22
|
+
|
|
23
|
+
# В случае, если используется простая проверка длинны списка (без вложенных матчеров сравнения больше или меньше)
|
|
24
|
+
if isinstance(match, int):
|
|
25
|
+
return {'length': match}
|
|
26
|
+
|
|
27
|
+
# В случае, если используется проверка длинны списка с вложенными матчерами типа 'greater_than' и 'less_than'.
|
|
28
|
+
# Не стал тут реализовывать обработку матчеров 'greater_than_or_equal_to' и 'less_than_or_equal_to',
|
|
29
|
+
# так как при проверке длинны списков они всегда могут быть заменены
|
|
30
|
+
# на более лаконичные 'greater_than' и 'less_than')
|
|
31
|
+
# Еще одна деталь, в том что 'minlength' / 'maxlength', больше сответствуют 'greater_than_or_equal_to' /
|
|
32
|
+
# 'less_than_or_equal_to', чем 'greater_than' / 'less_than'. Поэтому приходится прибавлять / удалять еденницу
|
|
33
|
+
elif isinstance(match, dict):
|
|
34
|
+
if match.get('greater_than'):
|
|
35
|
+
return {'minlength': match.get('greater_than') + 1}
|
|
36
|
+
elif match.get('less_than'):
|
|
37
|
+
return {'maxlength': match.get('less_than') - 1}
|
|
38
|
+
elif match.get('greater_than_or_equal_to'):
|
|
39
|
+
return {'minlength': match.get('greater_than_or_equal_to')}
|
|
40
|
+
elif match.get('less_than_or_equal_to'):
|
|
41
|
+
return {'maxlength': match.get('less_than_or_equal_to')}
|
|
42
|
+
else:
|
|
43
|
+
raise Exception(f"Нельзя использовать матчер `{match}` внутри матчера `has_length`")
|
|
44
|
+
else:
|
|
45
|
+
raise Exception(f"Для `has_item` получен матчер, для которого не задано "
|
|
46
|
+
f"правило преобразования в схему: {type(match)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_instance_of(atype):
|
|
50
|
+
""" Matches if object is an instance of, or inherits from, a given type """
|
|
51
|
+
value = atype.__name__
|
|
52
|
+
if value == 'str':
|
|
53
|
+
value = "string"
|
|
54
|
+
elif value == 'int':
|
|
55
|
+
value = "integer"
|
|
56
|
+
elif value == 'bool':
|
|
57
|
+
value = "boolean"
|
|
58
|
+
|
|
59
|
+
return {'type': value}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def empty():
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def none():
|
|
67
|
+
""" Matches if object is `None` """
|
|
68
|
+
return equal_to(None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def not_none():
|
|
72
|
+
""" Matches if object is not `None` """
|
|
73
|
+
return not_(None)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ------ Numbers ------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def greater_than(value):
|
|
79
|
+
""" Matches if object is greater than a given value """
|
|
80
|
+
_check_isinstance(value, (int, float))
|
|
81
|
+
return {'greater_than': value}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def greater_than_or_equal_to(value):
|
|
85
|
+
""" Matches if object is greater than or equal to a given value """
|
|
86
|
+
_check_isinstance(value, (int, float))
|
|
87
|
+
return {'greater_than_or_equal_to': value}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def less_than(value):
|
|
91
|
+
""" Matches if object is less_than a given value """
|
|
92
|
+
_check_isinstance(value, (int, float))
|
|
93
|
+
return {'less_than': value}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def less_than_or_equal_to(value):
|
|
97
|
+
""" Matches if object is less_than_or_equal_to a given value """
|
|
98
|
+
_check_isinstance(value, (int, float))
|
|
99
|
+
return {'less_than_or_equal_to': value}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def close_to(value, delta):
|
|
103
|
+
""" Matches if object is a number close to a given value, within a given delta """
|
|
104
|
+
_check_isinstance(value, (int, float))
|
|
105
|
+
_check_isinstance(delta, (int, float))
|
|
106
|
+
return {'close_to': (value, delta), 'type': 'integer'}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ------ Text ------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def starts_with(substring):
|
|
112
|
+
""" Matches if object is a string starting with a given string """
|
|
113
|
+
_check_isinstance(substring, str)
|
|
114
|
+
return {"starts_with": substring}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def contains_string(substring):
|
|
118
|
+
""" Matches if object is a string containing a given string. """
|
|
119
|
+
_check_isinstance(substring, str)
|
|
120
|
+
return {"contains_string": substring}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def string_contains_in_order(substrings):
|
|
124
|
+
""" Matches if object is a string containing a given list of substrings in relative order. """
|
|
125
|
+
_check_isinstance(substrings, tuple)
|
|
126
|
+
for i in substrings:
|
|
127
|
+
_check_isinstance(i, str)
|
|
128
|
+
return {"string_contains_in_order": substrings}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def ends_with(substring):
|
|
132
|
+
""" Matches if object is a string ending with a given string """
|
|
133
|
+
_check_isinstance(substring, str)
|
|
134
|
+
return {"ends_with": substring}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def equal_to_ignoring_case(substring):
|
|
138
|
+
""" Matches if object is a string equal to a given string, ignoring case differences """
|
|
139
|
+
_check_isinstance(substring, str)
|
|
140
|
+
return {"equal_to_ignoring_case": substring}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def matches_regexp(pattern):
|
|
144
|
+
""" Matches if object is a string containing a match for a given regular expression """
|
|
145
|
+
_check_isinstance(pattern, str)
|
|
146
|
+
return {'regex': pattern}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ------ Sequence ------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def has_item(matcher):
|
|
152
|
+
""" Matches if any element of sequence satisfies a given matcher """
|
|
153
|
+
# TODO (Кузнецов М.): Добавить проверку matcher
|
|
154
|
+
return {'type': 'list', 'required': True, 'empty': False, "has_item": matcher}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _not_has_item(matcher):
|
|
158
|
+
""" Matches if any element of sequence NOT satisfies a given matcher """
|
|
159
|
+
return {'type': 'list', 'required': True, 'empty': False, "not_has_item": matcher}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def every_item(matcher):
|
|
163
|
+
""" Matches if every element of sequence satisfies a given matcher """
|
|
164
|
+
# TODO (Кузнецов М.): Добавить проверку matcher
|
|
165
|
+
return {'type': 'list', 'required': True, 'empty': False, "every_item": matcher}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def has_item_at_index(index, matcher):
|
|
169
|
+
"""Проверить, что в списке элемент с переданным индексом удовлетворяет переданному матчеру."""
|
|
170
|
+
return {'type': 'list', 'required': True, 'empty': False, "has_item_at_index": (index, matcher)}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def has_first_item(matcher):
|
|
174
|
+
"""Проверить, что в списке первый элемент удовлетворяет переданному матчеру."""
|
|
175
|
+
return has_item_at_index(0, matcher)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def has_last_item(matcher):
|
|
179
|
+
"""Проверить, что в списке последний элемент удовлетворяет переданному матчеру."""
|
|
180
|
+
return has_item_at_index(-1, matcher)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def has_items(matchers):
|
|
184
|
+
""" Matches if any element of sequence satisfies a given matchers """
|
|
185
|
+
matchers_to_list_has_item = [extract(i) for i in matchers]
|
|
186
|
+
return {'type': 'list', 'required': True, 'empty': False, "has_items": [i['has_item'] for i in matchers_to_list_has_item]}
|
|
187
|
+
|
|
188
|
+
# ------ Dictionary ------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def has_entries(*keys_valuematchers, **kv_args):
|
|
191
|
+
schema = {}
|
|
192
|
+
|
|
193
|
+
def add_to_scheme(_dict):
|
|
194
|
+
for k, v in _dict.items():
|
|
195
|
+
if k not in schema:
|
|
196
|
+
schema.update({k: extract(v)})
|
|
197
|
+
else:
|
|
198
|
+
schema[k].update(extract(v))
|
|
199
|
+
|
|
200
|
+
if keys_valuematchers:
|
|
201
|
+
if len(keys_valuematchers) > 1 and not isinstance(keys_valuematchers[0], dict):
|
|
202
|
+
raise ValueError('has_entries requires key-value pairs or dict with matchers')
|
|
203
|
+
add_to_scheme(keys_valuematchers[0])
|
|
204
|
+
|
|
205
|
+
add_to_scheme(kv_args)
|
|
206
|
+
|
|
207
|
+
return {'schema': schema, 'type': 'dict'}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def has_key(key_match):
|
|
211
|
+
return {'has_key': key_match}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ------ Logical ------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def all_of(*items):
|
|
218
|
+
""" Matches if all of the given matchers evaluate to ``True`` """
|
|
219
|
+
# TODO (Кузнецов М.): Дописать проверку на то, что items должен быть список матчеров
|
|
220
|
+
return {'allof': items}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def any_of(*items):
|
|
224
|
+
""" Matches if any of the given matchers evaluate to ``True`` """
|
|
225
|
+
# TODO (Кузнецов М.): Дописать проверку то что items должен быть список матчеров
|
|
226
|
+
return {'anyof': items}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ------ Datetime ------------------------------------------------------
|
|
230
|
+
def is_today():
|
|
231
|
+
""" Матчер проверки, что дата является сегодняшней (без учета времени) """
|
|
232
|
+
return is_today_with_shift()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def is_tomorrow():
|
|
236
|
+
""" Матчер проверки, что дата является завтрашней (без учета времени) """
|
|
237
|
+
return is_today_with_shift(hours=24)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def is_yesterday():
|
|
241
|
+
""" Матчер проверки, что дата является вчерашней (без учета времени) """
|
|
242
|
+
return is_today_with_shift(hours=-24)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def is_today_with_shift(weeks=None, days=None, hours=None, message=None):
|
|
246
|
+
""" Матчер проверки, что дата является сегодняшней с заданным сдвигом """
|
|
247
|
+
return {"is_today_with_shift": {"weeks": weeks, "days": days, "hours": hours, "message": message}}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def is_greater_than_datetime(value):
|
|
251
|
+
""" Проверка, что дата более поздняя, чем ожидаемая (с учетом времени) """
|
|
252
|
+
return {"is_greater_than_datetime": value}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def is_less_than_datetime(value):
|
|
256
|
+
""" Проверка, что дата более ранняя, чем ожидаемая (с учетом времени) """
|
|
257
|
+
return {"is_less_than_datetime": value}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def is_equal_to_datetime(value):
|
|
261
|
+
""" Проверка, что дата соответствует ожидаемой с учетом времени """
|
|
262
|
+
return {"is_equal_to_datetime": value}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def is_greater_than_date(value):
|
|
266
|
+
""" Проверка, что дата более поздняя (без учета времени) """
|
|
267
|
+
return {"is_greater_than_date": value}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def is_less_than_date(value):
|
|
271
|
+
""" Проверка, что дата более ранняя (без учета времени) """
|
|
272
|
+
return {"is_less_than_date": value}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def is_equal_to_date(value):
|
|
276
|
+
""" Проверка, что дата соответствует ожидаемой (без учета времени) """
|
|
277
|
+
return {"is_equal_to_date": value}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def is_equal_to_date_ignoring_seconds(value, seconds=5):
|
|
281
|
+
""" Проверка, что дата соответствует ожидаемой с учетом допустимой погрешности с 5 секунд (по умолчанию)
|
|
282
|
+
В seconds можно задать допустимую погрешность
|
|
283
|
+
"""
|
|
284
|
+
return {"is_equal_to_date_ignoring_seconds": {"value": value, "seconds": seconds}}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ----------------- Original Cerberus matchers ---------------------------
|
|
288
|
+
def is_in(values):
|
|
289
|
+
return {'allowed': values}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ------ Negative ------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
class CerberusNot:
|
|
295
|
+
def __init__(self, value):
|
|
296
|
+
self.value = value
|
|
297
|
+
|
|
298
|
+
def __repr__(self):
|
|
299
|
+
return f'not({self.value})'
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
not_ = CerberusNot
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def negative(matcher):
|
|
306
|
+
def matcher_type(_type):
|
|
307
|
+
return _type == type(matcher).__name__
|
|
308
|
+
|
|
309
|
+
if matcher_type('IsSequenceContaining'):
|
|
310
|
+
return _not_has_item(extract(matcher.element_matcher))
|
|
311
|
+
|
|
312
|
+
result = {}
|
|
313
|
+
for k, v in extract(matcher).items():
|
|
314
|
+
# Есть встроеенные в cerberus матчеры, которые почти всегда автоматически применяются
|
|
315
|
+
# (типа `nullable`, 'required' и так как они булевые, то при их инверсии достаточно сделать отрицание)
|
|
316
|
+
if k in ['nullable']:
|
|
317
|
+
result.update({k: not v})
|
|
318
|
+
else:
|
|
319
|
+
result.update({k: not_(v)})
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _check_isinstance(value, *types):
|
|
324
|
+
for _type in types:
|
|
325
|
+
if not isinstance(value, _type):
|
|
326
|
+
raise TypeError(f"Матчер работает только с {_type}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def extract(v, equal=True):
|
|
330
|
+
""" Функция маппинга матчеров Hamcrest в матчеры Cerberus
|
|
331
|
+
:param v:
|
|
332
|
+
:param equal:
|
|
333
|
+
:return: Cerberus marcher
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def matcher_type(_type):
|
|
337
|
+
return _type == type(v).__name__
|
|
338
|
+
|
|
339
|
+
if isinstance(v, str) or isinstance(v, int) or isinstance(v, float) or isinstance(v, list) or isinstance(v, dict):
|
|
340
|
+
return equal_to(v)
|
|
341
|
+
elif matcher_type('IsEqual') or matcher_type('IsEqualWithDiff'):
|
|
342
|
+
return equal_to(v.object) if equal == True else v.object
|
|
343
|
+
elif matcher_type('StringContains'):
|
|
344
|
+
return contains_string(v.substring)
|
|
345
|
+
elif matcher_type('StringStartsWith'):
|
|
346
|
+
return starts_with(v.substring)
|
|
347
|
+
elif matcher_type('StringEndsWith'):
|
|
348
|
+
return ends_with(v.substring)
|
|
349
|
+
elif matcher_type('StringContainsInOrder'):
|
|
350
|
+
return string_contains_in_order(v.substrings)
|
|
351
|
+
elif matcher_type('IsEqualIgnoringCase'):
|
|
352
|
+
return equal_to_ignoring_case(v.original_string)
|
|
353
|
+
elif matcher_type('IsCloseTo'):
|
|
354
|
+
return close_to(v.value, v.delta)
|
|
355
|
+
elif matcher_type('HasLength'):
|
|
356
|
+
return has_length(extract(v.len_matcher, equal=False))
|
|
357
|
+
elif matcher_type('OrderingComparison'):
|
|
358
|
+
if v.comparison_description == 'greater than':
|
|
359
|
+
return greater_than(v.value)
|
|
360
|
+
elif v.comparison_description == 'less than':
|
|
361
|
+
return less_than(v.value)
|
|
362
|
+
elif v.comparison_description == 'greater than or equal to':
|
|
363
|
+
return greater_than_or_equal_to(v.value)
|
|
364
|
+
elif v.comparison_description == 'less than or equal to':
|
|
365
|
+
return less_than_or_equal_to(v.value)
|
|
366
|
+
elif matcher_type('IsSequenceContaining'):
|
|
367
|
+
return has_item(extract(v.element_matcher))
|
|
368
|
+
elif matcher_type('EveryItem'):
|
|
369
|
+
return every_item(extract(v.matcher))
|
|
370
|
+
elif matcher_type('IsDictContainingEntries'):
|
|
371
|
+
return has_entries(**{k: v for k, v in v.value_matchers})
|
|
372
|
+
elif matcher_type('IsDictContaining'):
|
|
373
|
+
raise Exception("Матчер `has_entry()` не портирован в Cerberus-формат. Это можно сделать, но в большинстве "
|
|
374
|
+
"случаев лучше использовать матчер `has_entries()`")
|
|
375
|
+
elif matcher_type('IsDictContainingValue'):
|
|
376
|
+
raise Exception("Матчер `has_value()` еще не портирован в Cerberus-формат")
|
|
377
|
+
elif matcher_type('IsEmpty'):
|
|
378
|
+
return has_length(0)
|
|
379
|
+
elif matcher_type('AllOf'):
|
|
380
|
+
return all_of(*[extract(i) for i in v.matchers])
|
|
381
|
+
elif matcher_type('AnyOf'):
|
|
382
|
+
return any_of(*[extract(i) for i in v.matchers])
|
|
383
|
+
elif matcher_type('Is'):
|
|
384
|
+
return extract(v.matcher)
|
|
385
|
+
elif matcher_type('IsInstanceOf'):
|
|
386
|
+
return is_instance_of(v.expected_type)
|
|
387
|
+
elif matcher_type('IsNone'):
|
|
388
|
+
return none()
|
|
389
|
+
elif matcher_type('IsDictContainingKey'):
|
|
390
|
+
return has_key(v.key_matcher.object)
|
|
391
|
+
elif matcher_type('StringMatchesPattern'):
|
|
392
|
+
return matches_regexp(v.pattern.pattern)
|
|
393
|
+
elif matcher_type('IsNot'):
|
|
394
|
+
return negative(v.matcher)
|
|
395
|
+
elif matcher_type('IsToday'):
|
|
396
|
+
return is_today_with_shift(**v.time_shift, message=v.expected_message)
|
|
397
|
+
elif matcher_type('DateComparison'):
|
|
398
|
+
if v.ignore_time is False:
|
|
399
|
+
if v.operation == 'greater':
|
|
400
|
+
return is_greater_than_datetime(v.actual_item)
|
|
401
|
+
elif v.operation == 'less':
|
|
402
|
+
return is_less_than_datetime(v.actual_item)
|
|
403
|
+
elif v.operation == 'equal':
|
|
404
|
+
return is_equal_to_datetime(v.actual_item)
|
|
405
|
+
elif v.ignore_time is True:
|
|
406
|
+
if v.operation == "greater":
|
|
407
|
+
return is_greater_than_date(v.actual_item)
|
|
408
|
+
elif v.operation == "less":
|
|
409
|
+
return is_less_than_date(v.actual_item)
|
|
410
|
+
elif v.operation == "equal":
|
|
411
|
+
return is_equal_to_date(v.actual_item)
|
|
412
|
+
if v.operation == "equal with error":
|
|
413
|
+
return is_equal_to_date_ignoring_seconds(v.actual_item, seconds=v.error_seconds)
|
|
414
|
+
elif matcher_type('HasItemAtIndex'):
|
|
415
|
+
return has_item_at_index(index=v.index, matcher=extract(v.matcher))
|
|
416
|
+
elif matcher_type('IsIn'):
|
|
417
|
+
return is_in(v.sequence)
|
|
418
|
+
elif matcher_type('IsSequenceContainingEvery'):
|
|
419
|
+
return has_items(v.matcher.matchers)
|
|
420
|
+
raise Exception(f"Не удалось найти правило маппинга Hamcrest-матчера '{type(v).__name__}' в Cerberus-матчер")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
def make_assertion_text(dict_with_errors):
|
|
4
|
+
""" Сформировать и выдать легко-читаемое сообщение об ошибке """
|
|
5
|
+
# TODO: (Кузнецов М) Тут нужен рефакторинг и описание
|
|
6
|
+
fail_icon = '*' # \N{Cross Mark}
|
|
7
|
+
# fail_icon = '\N{Cross Mark}'
|
|
8
|
+
error_texts_list = []
|
|
9
|
+
|
|
10
|
+
def to_pretty(ddict, level=0):
|
|
11
|
+
if isinstance(ddict, str):
|
|
12
|
+
error_texts_list.append(f" \n{' ' * level}{ddict}")
|
|
13
|
+
|
|
14
|
+
elif isinstance(ddict, dict):
|
|
15
|
+
for k, v in ddict.items():
|
|
16
|
+
# otstup = '\t' * level
|
|
17
|
+
otstup = ' ' * level
|
|
18
|
+
|
|
19
|
+
if isinstance(v, str):
|
|
20
|
+
error_texts_list.append(f" \n{otstup}{fail_icon} {k}: {v}")
|
|
21
|
+
elif isinstance(v, dict):
|
|
22
|
+
error_texts_list.append(f" \n{otstup}{fail_icon} {k}:")
|
|
23
|
+
to_pretty(v, level=level+1)
|
|
24
|
+
elif isinstance(v, list):
|
|
25
|
+
assert len(v) < 3
|
|
26
|
+
if len(v) == 1:
|
|
27
|
+
to_pretty({k:v[0]}, level=level)
|
|
28
|
+
else:
|
|
29
|
+
error_texts_list.append(f" \n{otstup}{fail_icon} {k}: {v[0]}")
|
|
30
|
+
to_pretty(v[1], level=level+1)
|
|
31
|
+
else:
|
|
32
|
+
raise TypeError("Не удалось распарсить результат работы cerberus")
|
|
33
|
+
|
|
34
|
+
else:
|
|
35
|
+
raise TypeError("Не удалось распарсить список ошибок")
|
|
36
|
+
|
|
37
|
+
to_pretty(dict_with_errors)
|
|
38
|
+
return error_texts_list
|