Build 060.26: complete Integration and Regression
This commit is contained in:
222
app/tests/support/live_trade_stream.py
Normal file
222
app/tests/support/live_trade_stream.py
Normal file
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.core.config import Settings
|
||||
|
||||
|
||||
RUN_LIVE_TESTS_ENV = "DZENTRA_RUN_LIVE_TESTS"
|
||||
LIVE_REST_URL_ENV = "DZENTRA_LIVE_REST_URL"
|
||||
LIVE_WEBSOCKET_URL_ENV = "DZENTRA_LIVE_WS_URL"
|
||||
LIVE_SYMBOLS_ENV = "DZENTRA_LIVE_SYMBOLS"
|
||||
LIVE_TRADE_TIMEOUT_ENV = "DZENTRA_LIVE_TRADE_TIMEOUT_SECONDS"
|
||||
|
||||
DEFAULT_LIVE_TRADE_TIMEOUT_SECONDS = 600.0
|
||||
LIVE_RUNTIME_CLEANUP_TIMEOUT_SECONDS = 30.0
|
||||
|
||||
|
||||
class LiveTestConfigurationError(ValueError):
|
||||
"""Ошибка явной конфигурации opt-in live verification."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiveTradeStreamTestConfig:
|
||||
rest_url: str
|
||||
websocket_url: str
|
||||
symbol: str
|
||||
trade_timeout_seconds: float
|
||||
|
||||
@property
|
||||
def scenario_timeout_seconds(self) -> float:
|
||||
return (
|
||||
self.trade_timeout_seconds * 2
|
||||
+ LIVE_RUNTIME_CLEANUP_TIMEOUT_SECONDS * 2
|
||||
)
|
||||
|
||||
|
||||
def load_live_trade_stream_test_config(
|
||||
environment: Mapping[str, str] | None = None,
|
||||
) -> LiveTradeStreamTestConfig | None:
|
||||
values = environment if environment is not None else os.environ
|
||||
opt_in = values.get(RUN_LIVE_TESTS_ENV, "").strip()
|
||||
|
||||
if opt_in in {"", "0"}:
|
||||
return None
|
||||
|
||||
if opt_in != "1":
|
||||
raise LiveTestConfigurationError(
|
||||
f"{RUN_LIVE_TESTS_ENV} must be exactly 1 when enabled."
|
||||
)
|
||||
|
||||
rest_url = _require_environment_value(
|
||||
values,
|
||||
LIVE_REST_URL_ENV,
|
||||
).rstrip("/")
|
||||
websocket_url = _require_environment_value(
|
||||
values,
|
||||
LIVE_WEBSOCKET_URL_ENV,
|
||||
)
|
||||
symbols_value = _require_environment_value(
|
||||
values,
|
||||
LIVE_SYMBOLS_ENV,
|
||||
)
|
||||
symbols = tuple(
|
||||
symbol.strip()
|
||||
for symbol in symbols_value.split(",")
|
||||
)
|
||||
|
||||
if any(not symbol for symbol in symbols):
|
||||
raise LiveTestConfigurationError(
|
||||
f"{LIVE_SYMBOLS_ENV} must not contain empty symbols."
|
||||
)
|
||||
|
||||
if len(symbols) != 1:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{LIVE_SYMBOLS_ENV} must contain exactly one symbol "
|
||||
"for bounded live verification."
|
||||
)
|
||||
|
||||
_validate_secure_url(
|
||||
rest_url,
|
||||
name=LIVE_REST_URL_ENV,
|
||||
expected_scheme="https",
|
||||
required_path=None,
|
||||
)
|
||||
_validate_secure_url(
|
||||
websocket_url,
|
||||
name=LIVE_WEBSOCKET_URL_ENV,
|
||||
expected_scheme="wss",
|
||||
required_path="/connect",
|
||||
)
|
||||
|
||||
timeout_seconds = _parse_positive_timeout(
|
||||
values.get(
|
||||
LIVE_TRADE_TIMEOUT_ENV,
|
||||
str(DEFAULT_LIVE_TRADE_TIMEOUT_SECONDS),
|
||||
),
|
||||
)
|
||||
|
||||
return LiveTradeStreamTestConfig(
|
||||
rest_url=rest_url,
|
||||
websocket_url=websocket_url,
|
||||
symbol=symbols[0],
|
||||
trade_timeout_seconds=timeout_seconds,
|
||||
)
|
||||
|
||||
|
||||
def build_live_trade_stream_settings(
|
||||
config: LiveTradeStreamTestConfig,
|
||||
) -> Settings:
|
||||
from src.core.config import Settings, TradeStreamSettings
|
||||
|
||||
return Settings(
|
||||
bot_token="live-verification-does-not-use-telegram",
|
||||
bot_parse_mode="HTML",
|
||||
app_env="live-verification",
|
||||
log_level="INFO",
|
||||
tz="UTC",
|
||||
exchange_enabled=True,
|
||||
exchange_name="dzengi",
|
||||
exchange_base_url=config.rest_url,
|
||||
exchange_ws_url="",
|
||||
exchange_api_key="",
|
||||
exchange_api_secret="",
|
||||
exchange_timeout_sec=20,
|
||||
exchange_testnet=False,
|
||||
default_symbol=config.symbol,
|
||||
trade_stream=TradeStreamSettings(
|
||||
enabled=True,
|
||||
websocket_url=config.websocket_url,
|
||||
symbols=(config.symbol,),
|
||||
open_timeout_seconds=10.0,
|
||||
probe_timeout_seconds=20.0,
|
||||
close_timeout_seconds=10.0,
|
||||
heartbeat_timeout_seconds=20.0,
|
||||
scheduler_interval_seconds=5.0,
|
||||
recovery_window_ms=3_599_999,
|
||||
),
|
||||
db_host="localhost",
|
||||
db_port=5432,
|
||||
db_name="live-verification",
|
||||
db_user="live-verification",
|
||||
db_password="",
|
||||
debug_enabled=False,
|
||||
journal_debug_enabled=False,
|
||||
)
|
||||
|
||||
|
||||
def _require_environment_value(
|
||||
environment: Mapping[str, str],
|
||||
name: str,
|
||||
) -> str:
|
||||
value = environment.get(name, "").strip()
|
||||
|
||||
if not value:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} is required when {RUN_LIVE_TESTS_ENV}=1."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _validate_secure_url(
|
||||
raw_url: str,
|
||||
*,
|
||||
name: str,
|
||||
expected_scheme: str,
|
||||
required_path: str | None,
|
||||
) -> None:
|
||||
parsed = urlsplit(raw_url)
|
||||
|
||||
if parsed.scheme.lower() != expected_scheme or not parsed.netloc:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} must be an absolute {expected_scheme} URL."
|
||||
)
|
||||
|
||||
if parsed.username is not None or parsed.password is not None:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} must not contain embedded credentials."
|
||||
)
|
||||
|
||||
if parsed.query or parsed.fragment:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} must not contain a query or fragment."
|
||||
)
|
||||
|
||||
if required_path is None:
|
||||
if parsed.path not in {"", "/"}:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} must be a base URL without an endpoint path."
|
||||
)
|
||||
return
|
||||
|
||||
if parsed.path.rstrip("/") != required_path:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{name} must end with {required_path}."
|
||||
)
|
||||
|
||||
|
||||
def _parse_positive_timeout(raw_value: str) -> float:
|
||||
try:
|
||||
timeout_seconds = float(raw_value.strip())
|
||||
except ValueError as error:
|
||||
raise LiveTestConfigurationError(
|
||||
f"{LIVE_TRADE_TIMEOUT_ENV} must be a number."
|
||||
) from error
|
||||
|
||||
if (
|
||||
not math.isfinite(timeout_seconds)
|
||||
or timeout_seconds <= 0
|
||||
):
|
||||
raise LiveTestConfigurationError(
|
||||
f"{LIVE_TRADE_TIMEOUT_ENV} must be positive and finite."
|
||||
)
|
||||
|
||||
return timeout_seconds
|
||||
Reference in New Issue
Block a user