persona-dsl 26.1.20.8__py3-none-any.whl
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.
- persona_dsl/__init__.py +35 -0
- persona_dsl/components/action.py +10 -0
- persona_dsl/components/base_step.py +251 -0
- persona_dsl/components/combined_step.py +68 -0
- persona_dsl/components/expectation.py +10 -0
- persona_dsl/components/fact.py +10 -0
- persona_dsl/components/goal.py +10 -0
- persona_dsl/components/ops.py +7 -0
- persona_dsl/components/step.py +75 -0
- persona_dsl/expectations/generic/__init__.py +15 -0
- persona_dsl/expectations/generic/contains_item.py +19 -0
- persona_dsl/expectations/generic/contains_the_text.py +15 -0
- persona_dsl/expectations/generic/has_entries.py +21 -0
- persona_dsl/expectations/generic/is_equal.py +24 -0
- persona_dsl/expectations/generic/is_greater_than.py +18 -0
- persona_dsl/expectations/generic/path_equal.py +27 -0
- persona_dsl/expectations/web/__init__.py +5 -0
- persona_dsl/expectations/web/is_displayed.py +13 -0
- persona_dsl/expectations/web/matches_aria_snapshot.py +221 -0
- persona_dsl/expectations/web/matches_screenshot.py +160 -0
- persona_dsl/generators/__init__.py +5 -0
- persona_dsl/generators/api_generator.py +423 -0
- persona_dsl/generators/cli.py +431 -0
- persona_dsl/generators/page_generator.py +1140 -0
- persona_dsl/ops/api/__init__.py +5 -0
- persona_dsl/ops/api/json_as.py +104 -0
- persona_dsl/ops/api/json_response.py +48 -0
- persona_dsl/ops/api/send_request.py +41 -0
- persona_dsl/ops/db/__init__.py +5 -0
- persona_dsl/ops/db/execute_sql.py +22 -0
- persona_dsl/ops/db/fetch_all.py +29 -0
- persona_dsl/ops/db/fetch_one.py +22 -0
- persona_dsl/ops/kafka/__init__.py +4 -0
- persona_dsl/ops/kafka/message_in_topic.py +89 -0
- persona_dsl/ops/kafka/send_message.py +35 -0
- persona_dsl/ops/soap/__init__.py +4 -0
- persona_dsl/ops/soap/call_operation.py +24 -0
- persona_dsl/ops/soap/operation_result.py +24 -0
- persona_dsl/ops/web/__init__.py +37 -0
- persona_dsl/ops/web/aria_snapshot.py +87 -0
- persona_dsl/ops/web/click.py +30 -0
- persona_dsl/ops/web/current_path.py +17 -0
- persona_dsl/ops/web/element_attribute.py +24 -0
- persona_dsl/ops/web/element_is_visible.py +27 -0
- persona_dsl/ops/web/element_text.py +28 -0
- persona_dsl/ops/web/elements_count.py +42 -0
- persona_dsl/ops/web/fill.py +41 -0
- persona_dsl/ops/web/generate_page_object.py +118 -0
- persona_dsl/ops/web/input_value.py +23 -0
- persona_dsl/ops/web/navigate.py +52 -0
- persona_dsl/ops/web/press_key.py +37 -0
- persona_dsl/ops/web/rich_aria_snapshot.py +146 -0
- persona_dsl/ops/web/screenshot.py +68 -0
- persona_dsl/ops/web/table_data.py +43 -0
- persona_dsl/ops/web/wait_for_navigation.py +23 -0
- persona_dsl/pages/__init__.py +133 -0
- persona_dsl/pages/elements.py +998 -0
- persona_dsl/pages/page.py +44 -0
- persona_dsl/pages/virtual_page.py +94 -0
- persona_dsl/persona.py +125 -0
- persona_dsl/pytest_plugin.py +1064 -0
- persona_dsl/runtime/dist/persona_bundle.js +1077 -0
- persona_dsl/skills/__init__.py +7 -0
- persona_dsl/skills/core/base.py +41 -0
- persona_dsl/skills/core/skill_definition.py +30 -0
- persona_dsl/skills/use_api.py +251 -0
- persona_dsl/skills/use_browser.py +78 -0
- persona_dsl/skills/use_database.py +129 -0
- persona_dsl/skills/use_kafka.py +135 -0
- persona_dsl/skills/use_soap.py +66 -0
- persona_dsl/utils/__init__.py +0 -0
- persona_dsl/utils/artifacts.py +22 -0
- persona_dsl/utils/config.py +54 -0
- persona_dsl/utils/data_providers.py +159 -0
- persona_dsl/utils/decorators.py +80 -0
- persona_dsl/utils/metrics.py +69 -0
- persona_dsl/utils/naming.py +14 -0
- persona_dsl/utils/path.py +202 -0
- persona_dsl/utils/retry.py +51 -0
- persona_dsl/utils/taas_integration.py +124 -0
- persona_dsl/utils/waits.py +112 -0
- persona_dsl-26.1.20.8.dist-info/METADATA +35 -0
- persona_dsl-26.1.20.8.dist-info/RECORD +86 -0
- persona_dsl-26.1.20.8.dist-info/WHEEL +5 -0
- persona_dsl-26.1.20.8.dist-info/entry_points.txt +6 -0
- persona_dsl-26.1.20.8.dist-info/top_level.txt +1 -0
persona_dsl/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .components.action import Action
|
|
2
|
+
from .utils.decorators import data_driven, tag, parametrize_simple
|
|
3
|
+
from .persona import BaseRole
|
|
4
|
+
from .skills.core.skill_definition import BaseSkill
|
|
5
|
+
from .components.expectation import Expectation
|
|
6
|
+
from .components.fact import Fact
|
|
7
|
+
from .components.goal import Goal
|
|
8
|
+
from .persona import Persona
|
|
9
|
+
from .skills.core.skill_definition import SkillId
|
|
10
|
+
from .utils.waits import wait_until
|
|
11
|
+
from .skills.core.base import Skill
|
|
12
|
+
from .skills import UseAPI, UseBrowser, UseDatabase, UseKafka, UseSOAP
|
|
13
|
+
from .components.step import Step
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Action",
|
|
17
|
+
"Expectation",
|
|
18
|
+
"Goal",
|
|
19
|
+
"Persona",
|
|
20
|
+
"Fact",
|
|
21
|
+
"SkillId",
|
|
22
|
+
"Step",
|
|
23
|
+
"data_driven",
|
|
24
|
+
"tag",
|
|
25
|
+
"parametrize_simple",
|
|
26
|
+
"BaseRole",
|
|
27
|
+
"BaseSkill",
|
|
28
|
+
"Skill",
|
|
29
|
+
"wait_until",
|
|
30
|
+
"UseAPI",
|
|
31
|
+
"UseBrowser",
|
|
32
|
+
"UseDatabase",
|
|
33
|
+
"UseKafka",
|
|
34
|
+
"UseSOAP",
|
|
35
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
from allure import step
|
|
9
|
+
|
|
10
|
+
from ..utils.artifacts import artifacts_dir
|
|
11
|
+
from ..utils.metrics import metrics
|
|
12
|
+
from ..utils.path import extract_by_path
|
|
13
|
+
from ..utils.taas_integration import publish_taas_event
|
|
14
|
+
from ..utils.retry import RetryPolicy
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", bound="BaseStep")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseStep(ABC):
|
|
22
|
+
"""Единый базовый класс для всех исполняемых компонентов."""
|
|
23
|
+
|
|
24
|
+
# Политика ретраев по умолчанию для класса (может быть переопределена в наследниках)
|
|
25
|
+
retry_policy: Optional[RetryPolicy] = None
|
|
26
|
+
|
|
27
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
28
|
+
self._instance_retry_policy: Optional[RetryPolicy] = None
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
32
|
+
param_str = ", ".join(f"{k}={v!r}" for k, v in params.items())
|
|
33
|
+
return f"{self.__class__.__name__}({param_str})"
|
|
34
|
+
|
|
35
|
+
def with_retry(self: T, policy: RetryPolicy) -> T:
|
|
36
|
+
"""
|
|
37
|
+
Возвращает копию шага с установленной политикой ретраев.
|
|
38
|
+
Полезно для ad-hoc настройки надежности в конкретном сценарии.
|
|
39
|
+
"""
|
|
40
|
+
import copy
|
|
41
|
+
|
|
42
|
+
new_step = copy.copy(self)
|
|
43
|
+
new_step._instance_retry_policy = policy
|
|
44
|
+
return new_step
|
|
45
|
+
|
|
46
|
+
def _get_effective_retry_policy(self, persona: Any) -> RetryPolicy:
|
|
47
|
+
"""Определяет актуальную политику ретраев (Instance > Class > Global Default)."""
|
|
48
|
+
# 1. Instance level (через .with_retry())
|
|
49
|
+
instance_policy = getattr(self, "_instance_retry_policy", None)
|
|
50
|
+
if instance_policy:
|
|
51
|
+
return instance_policy
|
|
52
|
+
|
|
53
|
+
# 2. Class level (атрибут класса)
|
|
54
|
+
if self.retry_policy:
|
|
55
|
+
return self.retry_policy
|
|
56
|
+
|
|
57
|
+
# 3. Global default from Persona config
|
|
58
|
+
# Ожидаем, что в persona.retries_config лежит dict, например:
|
|
59
|
+
# {'default': {...}, 'web_actions': {...}}
|
|
60
|
+
if hasattr(persona, "retries_config") and persona.retries_config:
|
|
61
|
+
# Можно усложнить логику и искать по типу шага (web_actions и т.д.)
|
|
62
|
+
# Пока берем 'default'
|
|
63
|
+
default_cfg = persona.retries_config.get("default")
|
|
64
|
+
if default_cfg:
|
|
65
|
+
return RetryPolicy(**default_cfg)
|
|
66
|
+
|
|
67
|
+
# 4. Hard fallback
|
|
68
|
+
return RetryPolicy(max_attempts=1)
|
|
69
|
+
|
|
70
|
+
def _get_component_type(self) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Определяет тип компонента на основе иерархии классов.
|
|
73
|
+
"""
|
|
74
|
+
# Локальные импорты для разрыва циклической зависимости
|
|
75
|
+
from .goal import Goal
|
|
76
|
+
from .fact import Fact
|
|
77
|
+
from .action import Action
|
|
78
|
+
from .expectation import Expectation
|
|
79
|
+
from .combined_step import CombinedStep
|
|
80
|
+
from .step import Step
|
|
81
|
+
from .ops import Ops
|
|
82
|
+
|
|
83
|
+
if isinstance(self, Goal):
|
|
84
|
+
return "goal"
|
|
85
|
+
if isinstance(self, Fact):
|
|
86
|
+
return "fact"
|
|
87
|
+
if isinstance(self, Action):
|
|
88
|
+
return "action"
|
|
89
|
+
if isinstance(self, Expectation):
|
|
90
|
+
return "check"
|
|
91
|
+
if isinstance(self, CombinedStep):
|
|
92
|
+
return "combined_step"
|
|
93
|
+
if isinstance(self, Step):
|
|
94
|
+
return "step"
|
|
95
|
+
if isinstance(self, Ops):
|
|
96
|
+
if hasattr(self, "_component_type"):
|
|
97
|
+
comp_type = getattr(self, "_component_type")
|
|
98
|
+
if isinstance(comp_type, str):
|
|
99
|
+
return comp_type
|
|
100
|
+
return "ops"
|
|
101
|
+
return "unknown"
|
|
102
|
+
|
|
103
|
+
def execute(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
104
|
+
"""
|
|
105
|
+
Универсальная обертка для выполнения шага с поддержкой ретраев.
|
|
106
|
+
"""
|
|
107
|
+
description = self._get_step_description(persona)
|
|
108
|
+
start_time = time.time()
|
|
109
|
+
component_type = self._get_component_type()
|
|
110
|
+
policy = self._get_effective_retry_policy(persona)
|
|
111
|
+
|
|
112
|
+
params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
|
|
113
|
+
publish_taas_event(
|
|
114
|
+
{
|
|
115
|
+
"event": "step_start",
|
|
116
|
+
"type": component_type,
|
|
117
|
+
"data": {
|
|
118
|
+
"description": description,
|
|
119
|
+
"timestamp": start_time,
|
|
120
|
+
"component_name": self.__class__.__name__,
|
|
121
|
+
"component_params": params,
|
|
122
|
+
"max_attempts": policy.max_attempts,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
value: Any = None
|
|
128
|
+
status = "passed"
|
|
129
|
+
attempt = 0
|
|
130
|
+
attempt = 0
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Цикл ретраев
|
|
134
|
+
while True:
|
|
135
|
+
attempt += 1
|
|
136
|
+
try:
|
|
137
|
+
with step(
|
|
138
|
+
description
|
|
139
|
+
if attempt == 1
|
|
140
|
+
else f"{description} (попытка {attempt})"
|
|
141
|
+
):
|
|
142
|
+
if attempt > 1 and policy.on_retry_action:
|
|
143
|
+
step("Предварительное действие перед повтором")
|
|
144
|
+
policy.execute_action()
|
|
145
|
+
|
|
146
|
+
value = self._perform(persona, *args, **kwargs)
|
|
147
|
+
break # Успех
|
|
148
|
+
except Exception as e:
|
|
149
|
+
if policy.should_retry(attempt, e):
|
|
150
|
+
logger.warning(
|
|
151
|
+
f"Шаг '{description}' упал (попытка {attempt}/{policy.max_attempts}): {e}. Ретрай через {policy.delay}с."
|
|
152
|
+
)
|
|
153
|
+
policy.wait(attempt)
|
|
154
|
+
else:
|
|
155
|
+
raise e
|
|
156
|
+
|
|
157
|
+
# Привязка результата
|
|
158
|
+
save_as = getattr(self, "save_as", None)
|
|
159
|
+
extract = getattr(self, "extract", None)
|
|
160
|
+
if save_as:
|
|
161
|
+
bound_value = (
|
|
162
|
+
extract_by_path(value, extract)
|
|
163
|
+
if isinstance(extract, str)
|
|
164
|
+
else value
|
|
165
|
+
)
|
|
166
|
+
persona.memory[save_as] = bound_value
|
|
167
|
+
|
|
168
|
+
if value is not None:
|
|
169
|
+
with step(f"Получено значение: {str(value)[:100]}"):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
status = "failed"
|
|
176
|
+
raise
|
|
177
|
+
finally:
|
|
178
|
+
end_time = time.time()
|
|
179
|
+
duration = end_time - start_time
|
|
180
|
+
metrics.gauge(
|
|
181
|
+
"step.duration",
|
|
182
|
+
duration,
|
|
183
|
+
tags={
|
|
184
|
+
"type": component_type,
|
|
185
|
+
"name": self.__class__.__name__,
|
|
186
|
+
"status": status,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
event_data_end: dict[str, Any] = {
|
|
191
|
+
"description": description,
|
|
192
|
+
"timestamp": end_time,
|
|
193
|
+
"duration": duration,
|
|
194
|
+
"status": status,
|
|
195
|
+
"attempts_used": attempt,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
result_raw: Any = None
|
|
199
|
+
result_raw_path: str | None = None
|
|
200
|
+
result_raw_url: str | None = None
|
|
201
|
+
if isinstance(value, (dict, list)):
|
|
202
|
+
result_raw = value
|
|
203
|
+
raw_dir = artifacts_dir("result_raw")
|
|
204
|
+
filename = f"{int(end_time * 1000)}-{self.__class__.__name__}.json"
|
|
205
|
+
file_path = raw_dir / filename
|
|
206
|
+
try:
|
|
207
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
208
|
+
json.dump(result_raw, f, ensure_ascii=False, indent=2)
|
|
209
|
+
result_raw_path = str(file_path)
|
|
210
|
+
run_id = os.environ.get("TAAS_RUN_ID")
|
|
211
|
+
result_raw_url = (
|
|
212
|
+
f"/artifacts/{run_id}/result_raw/{filename}" if run_id else None
|
|
213
|
+
)
|
|
214
|
+
except (IOError, TypeError) as e:
|
|
215
|
+
logger.warning(
|
|
216
|
+
f"Не удалось сохранить result_raw артефакт в {file_path}: {e}"
|
|
217
|
+
)
|
|
218
|
+
result_raw = None
|
|
219
|
+
|
|
220
|
+
event_data_end.update(
|
|
221
|
+
{
|
|
222
|
+
"result": str(value)[:100] if value is not None else "None",
|
|
223
|
+
"result_raw": result_raw,
|
|
224
|
+
"bindings": {
|
|
225
|
+
"save_as": getattr(self, "save_as", None),
|
|
226
|
+
"extract": getattr(self, "extract", None),
|
|
227
|
+
},
|
|
228
|
+
"result_raw_path": result_raw_path,
|
|
229
|
+
"result_raw_url": result_raw_url,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
publish_taas_event(
|
|
234
|
+
{
|
|
235
|
+
"event": "step_end",
|
|
236
|
+
"type": component_type,
|
|
237
|
+
"data": event_data_end,
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def _get_step_description(self, persona: Any) -> str: # pragma: no cover
|
|
243
|
+
"""Возвращает текстовое описание шага для Allure."""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
@abstractmethod
|
|
247
|
+
def _perform(
|
|
248
|
+
self, persona: Any, *args: Any, **kwargs: Any
|
|
249
|
+
) -> Any: # pragma: no cover
|
|
250
|
+
"""Содержит реальную логику выполнения шага."""
|
|
251
|
+
...
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Any, Generator
|
|
3
|
+
|
|
4
|
+
from .base_step import BaseStep
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CombinedStep(BaseStep):
|
|
8
|
+
"""
|
|
9
|
+
Базовый класс для высокоуровневого, композитного шага (Combined Step),
|
|
10
|
+
который объединяет последовательность других шагов (Steps) для достижения
|
|
11
|
+
бизнес-цели. Управляется через генератор.
|
|
12
|
+
|
|
13
|
+
Также предоставляет метод make(persona) для story-стиля, который
|
|
14
|
+
просто вызывает execute(persona) без добавления логики.
|
|
15
|
+
|
|
16
|
+
Основной контракт — реализация генераторного метода _run(), который
|
|
17
|
+
позволяет описывать как простые последовательности Steps, так и сложную
|
|
18
|
+
логику с передачей результатов между ними.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def make(self, persona: Any) -> None:
|
|
22
|
+
self.execute(persona)
|
|
23
|
+
|
|
24
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Исполняет генераторный сценарий из `_run`, передавая результаты
|
|
27
|
+
выполнения Steps обратно в генератор.
|
|
28
|
+
"""
|
|
29
|
+
from .step import Step
|
|
30
|
+
|
|
31
|
+
gen = self._run(persona, *args, **kwargs)
|
|
32
|
+
try:
|
|
33
|
+
# Первый вызов для запуска генератора и получения первого шага
|
|
34
|
+
step_to_execute = gen.send(None)
|
|
35
|
+
while True:
|
|
36
|
+
# Проверяем, что из CombinedStep не yield'ят низко-уровневые Ops
|
|
37
|
+
if not isinstance(step_to_execute, (Step, CombinedStep)):
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"Компонент '{self.__class__.__name__}' (наследник CombinedStep) "
|
|
40
|
+
f"может yield'ить только другие шаги (наследники Step/CombinedStep), "
|
|
41
|
+
f"но был получен объект типа {type(step_to_execute).__name__}."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Выполняем полученный шаг
|
|
45
|
+
last_result = step_to_execute.execute(persona)
|
|
46
|
+
# Отправляем результат обратно в генератор и получаем следующий шаг
|
|
47
|
+
step_to_execute = gen.send(last_result)
|
|
48
|
+
except StopIteration as e:
|
|
49
|
+
# Генератор завершился (оператором return), его результат в e.value
|
|
50
|
+
return e.value
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def _get_step_description(self, persona: Any) -> str: # pragma: no cover
|
|
54
|
+
"""Возвращает текстовое описание шага для Allure."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def _run(
|
|
59
|
+
self, persona: Any, *args: Any, **kwargs: Any
|
|
60
|
+
) -> Generator[Any, Any, Any]: # pragma: no cover
|
|
61
|
+
"""
|
|
62
|
+
Декларативно-императивный контракт.
|
|
63
|
+
Yield'ит последовательность Steps и явно возвращает результат.
|
|
64
|
+
"""
|
|
65
|
+
# Этот код нужен, чтобы сделать метод абстрактным генератором.
|
|
66
|
+
# В реальных реализациях он будет заменён.
|
|
67
|
+
if False:
|
|
68
|
+
yield
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Any, Generator
|
|
2
|
+
|
|
3
|
+
from .base_step import BaseStep
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Step(BaseStep):
|
|
7
|
+
"""
|
|
8
|
+
Базовый класс для доменно-ориентированных шагов (Steps),
|
|
9
|
+
которые инкапсулируют бизнес-логику и состоят из последовательности
|
|
10
|
+
низкоуровневых операций (Ops), управляемых через генератор.
|
|
11
|
+
|
|
12
|
+
Предоставляет универсальные методы-обёртки для разных стилей написания:
|
|
13
|
+
- perform_as(persona): story-стиль для действий
|
|
14
|
+
- get(persona): story-стиль для фактов (возвращает значение)
|
|
15
|
+
- check(persona, actual_value): story-стиль для проверок
|
|
16
|
+
|
|
17
|
+
Основной контракт — реализация генераторного метода _run(), который
|
|
18
|
+
позволяет описывать как простые последовательности Ops, так и сложную
|
|
19
|
+
логику с передачей результатов между операциями.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def perform_as(self, persona: Any) -> None:
|
|
23
|
+
self.execute(persona)
|
|
24
|
+
|
|
25
|
+
def get(self, persona: Any) -> Any:
|
|
26
|
+
return self.execute(persona)
|
|
27
|
+
|
|
28
|
+
def check(self, persona: Any, actual_value: Any) -> None:
|
|
29
|
+
self.execute(persona, actual_value)
|
|
30
|
+
|
|
31
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
"""
|
|
33
|
+
Исполняет генераторный сценарий из `_run`, передавая результаты
|
|
34
|
+
выполнения Ops обратно в генератор.
|
|
35
|
+
"""
|
|
36
|
+
from .ops import Ops
|
|
37
|
+
|
|
38
|
+
gen = self._run(persona, *args, **kwargs)
|
|
39
|
+
try:
|
|
40
|
+
# Первый вызов для запуска генератора и получения первой операции
|
|
41
|
+
op_to_execute = gen.send(None)
|
|
42
|
+
while True:
|
|
43
|
+
# Проверяем, что из Step yield'ят только Ops
|
|
44
|
+
if not isinstance(op_to_execute, Ops):
|
|
45
|
+
raise TypeError(
|
|
46
|
+
f"Компонент '{self.__class__.__name__}' (наследник Step) "
|
|
47
|
+
f"может yield'ить только объекты-наследники Ops, "
|
|
48
|
+
f"но был получен объект типа {type(op_to_execute).__name__}."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Выполняем полученную операцию
|
|
52
|
+
current_result = op_to_execute.execute(persona)
|
|
53
|
+
# Отправляем результат обратно в генератор и получаем следующую операцию
|
|
54
|
+
op_to_execute = gen.send(current_result)
|
|
55
|
+
except StopIteration as e:
|
|
56
|
+
# Генератор завершился (оператором return), его результат в e.value
|
|
57
|
+
return e.value
|
|
58
|
+
|
|
59
|
+
def _run(self, persona: Any, *args: Any, **kwargs: Any) -> Generator[Any, Any, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Декларативно-императивный контракт.
|
|
62
|
+
Yield'ит последовательность Ops и явно возвращает результат.
|
|
63
|
+
|
|
64
|
+
По умолчанию выбрасывает NotImplementedError. Наследники должны либо:
|
|
65
|
+
1. Реализовать этот метод (генераторный стиль).
|
|
66
|
+
2. Переопределить _perform (императивный стиль).
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError(
|
|
69
|
+
f"Класс {self.__class__.__name__} должен реализовать метод '_run' (генераторный стиль) "
|
|
70
|
+
f"или переопределить метод '_perform' (императивный стиль)."
|
|
71
|
+
)
|
|
72
|
+
# Этот yield нужен, чтобы Python считал метод генератором (даже если он сразу падает)
|
|
73
|
+
# Это важно для статических анализаторов и некоторых проверок.
|
|
74
|
+
if False:
|
|
75
|
+
yield
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .is_equal import IsEqualTo
|
|
2
|
+
from .contains_the_text import ContainsTheText
|
|
3
|
+
from .has_entries import HasEntries
|
|
4
|
+
from .contains_item import ContainsItem
|
|
5
|
+
from .is_greater_than import IsGreaterThan
|
|
6
|
+
from .path_equal import PathEqual
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"IsEqualTo",
|
|
10
|
+
"ContainsTheText",
|
|
11
|
+
"HasEntries",
|
|
12
|
+
"ContainsItem",
|
|
13
|
+
"IsGreaterThan",
|
|
14
|
+
"PathEqual",
|
|
15
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.expectation import Expectation
|
|
4
|
+
from hamcrest import assert_that, has_item
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ContainsItem(Expectation):
|
|
8
|
+
"""Проверяет, что список содержит указанный элемент."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, expected_item: Any):
|
|
11
|
+
self.expected_item = expected_item
|
|
12
|
+
|
|
13
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
14
|
+
"""Проверяет, что список содержит указанный элемент."""
|
|
15
|
+
actual_list = args[0]
|
|
16
|
+
assert_that(actual_list, has_item(self.expected_item))
|
|
17
|
+
|
|
18
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
19
|
+
return f"содержит элемент '{self.expected_item}'"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from persona_dsl.components.expectation import Expectation
|
|
3
|
+
from hamcrest import assert_that, contains_string
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContainsTheText(Expectation):
|
|
7
|
+
def __init__(self, expected_text: str):
|
|
8
|
+
self.expected_text = expected_text
|
|
9
|
+
|
|
10
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
11
|
+
actual_text = args[0]
|
|
12
|
+
assert_that(actual_text, contains_string(self.expected_text))
|
|
13
|
+
|
|
14
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
15
|
+
return f"содержит текст '{self.expected_text}'"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
from persona_dsl.components.expectation import Expectation
|
|
3
|
+
from hamcrest import assert_that, has_entries
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HasEntries(Expectation):
|
|
7
|
+
"""
|
|
8
|
+
Проверяет, что фактический словарь содержит все ключи и значения
|
|
9
|
+
из ожидаемого словаря.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, expected_data: Dict[str, Any]):
|
|
13
|
+
self.expected_data = expected_data
|
|
14
|
+
|
|
15
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
16
|
+
"""Проверяет, что actual_data содержит все записи из expected_data."""
|
|
17
|
+
actual_data = args[0]
|
|
18
|
+
assert_that(actual_data, has_entries(self.expected_data))
|
|
19
|
+
|
|
20
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
21
|
+
return f"содержит записи {self.expected_data}"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.expectation import Expectation
|
|
4
|
+
from hamcrest import assert_that, equal_to
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class IsEqualTo(Expectation):
|
|
8
|
+
"""Проверяет, что фактическое значение равно ожидаемому."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, expected: Any):
|
|
11
|
+
"""Инициализирует ожидание.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
expected: Ожидаемое значение.
|
|
15
|
+
"""
|
|
16
|
+
self.expected = expected
|
|
17
|
+
|
|
18
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
19
|
+
"""Проверяет, что фактическое значение равно ожидаемому."""
|
|
20
|
+
actual = args[0]
|
|
21
|
+
assert_that(actual, equal_to(self.expected))
|
|
22
|
+
|
|
23
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
24
|
+
return f"равно '{self.expected}'"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from persona_dsl.components.expectation import Expectation
|
|
3
|
+
from hamcrest import assert_that, greater_than
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IsGreaterThan(Expectation):
|
|
7
|
+
"""Проверяет, что одно число больше другого."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, value: int | float):
|
|
10
|
+
self.value = value
|
|
11
|
+
|
|
12
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
13
|
+
"""Проверяет, что одно число больше другого."""
|
|
14
|
+
actual_value = args[0]
|
|
15
|
+
assert_that(actual_value, greater_than(self.value))
|
|
16
|
+
|
|
17
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
18
|
+
return f"больше чем {self.value}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from hamcrest import assert_that, equal_to
|
|
4
|
+
|
|
5
|
+
from persona_dsl.components.expectation import Expectation
|
|
6
|
+
from persona_dsl.utils.path import extract_by_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PathEqual(Expectation):
|
|
10
|
+
"""
|
|
11
|
+
Проверяет, что значение по указанному пути (dot/bracket нотация) внутри объекта равно ожидаемому.
|
|
12
|
+
|
|
13
|
+
Пример:
|
|
14
|
+
PathEqual("a.b[0].c", 42).check({"a": {"b": [{"c": 42}]}}) # проходит
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: str, expected: Any):
|
|
18
|
+
self.path = path
|
|
19
|
+
self.expected = expected
|
|
20
|
+
|
|
21
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
22
|
+
actual = args[0]
|
|
23
|
+
value = extract_by_path(actual, self.path)
|
|
24
|
+
assert_that(value, equal_to(self.expected))
|
|
25
|
+
|
|
26
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
27
|
+
return f"значение по пути '{self.path}' равно '{self.expected}'"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from persona_dsl.components.expectation import Expectation
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class IsDisplayed(Expectation):
|
|
6
|
+
"""Проверяет, что значение (результат факта) равно True."""
|
|
7
|
+
|
|
8
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
9
|
+
actual_value = args[0]
|
|
10
|
+
assert actual_value is True, "Элемент не отображается, хотя ожидалось обратное."
|
|
11
|
+
|
|
12
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
13
|
+
return "отображается на странице"
|