mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 15:44:15 +08:00
Harden transport and decode paths
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
"""真实通达信服务器 smoke test。
|
||||
|
||||
默认跳过;设置 XMTDX_LIVE=1 后执行。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from xmtdx import AsyncTdxClient, Market, TdxClient
|
||||
|
||||
_LIVE_ENABLED = os.getenv("XMTDX_LIVE") == "1"
|
||||
_LIVE_HOST = os.getenv("XMTDX_HOST", "180.153.18.170")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _LIVE_ENABLED,
|
||||
reason="set XMTDX_LIVE=1 to run live integration tests",
|
||||
)
|
||||
|
||||
|
||||
def test_sync_live_smoke() -> None:
|
||||
with TdxClient(_LIVE_HOST, timeout=5.0) as client:
|
||||
assert client.get_security_count(Market.SH) > 0
|
||||
assert client.get_security_count(Market.SZ) > 0
|
||||
|
||||
|
||||
def test_async_live_smoke() -> None:
|
||||
async def main() -> None:
|
||||
async with AsyncTdxClient(_LIVE_HOST, timeout=5.0) as client:
|
||||
assert await client.get_security_count(Market.SH) > 0
|
||||
assert await client.get_security_count(Market.SZ) > 0
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,139 @@
|
||||
"""异步 transport 回归测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import struct
|
||||
import time
|
||||
|
||||
from xmtdx import AsyncTdxClient, Market
|
||||
from xmtdx.commands.security_count import GetSecurityCountCmd
|
||||
from xmtdx.commands.setup import SETUP_COMMANDS
|
||||
from xmtdx.exceptions import TdxConnectionError
|
||||
|
||||
|
||||
def _pack_frame(body: bytes) -> bytes:
|
||||
return struct.pack("<IIIHH", 0, 0, 0, len(body), len(body)) + body
|
||||
|
||||
|
||||
def test_async_client_serializes_concurrent_calls() -> None:
|
||||
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
|
||||
|
||||
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
try:
|
||||
for setup_cmd in SETUP_COMMANDS:
|
||||
await reader.readexactly(len(setup_cmd))
|
||||
writer.write(_pack_frame(b""))
|
||||
await writer.drain()
|
||||
|
||||
await reader.readexactly(request_len)
|
||||
writer.write(_pack_frame(struct.pack("<H", 5)))
|
||||
await writer.drain()
|
||||
|
||||
await reader.readexactly(request_len)
|
||||
writer.write(_pack_frame(struct.pack("<H", 6)))
|
||||
await writer.drain()
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def main() -> None:
|
||||
server = await asyncio.start_server(handle, "127.0.0.1", 0)
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
try:
|
||||
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.2)
|
||||
await client.connect()
|
||||
try:
|
||||
sh_count, sz_count = await asyncio.gather(
|
||||
client.get_security_count(Market.SH),
|
||||
client.get_security_count(Market.SZ),
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
finally:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
assert sh_count == 5
|
||||
assert sz_count == 6
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_async_client_auto_reconnect() -> None:
|
||||
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
|
||||
connection_ids: list[int] = []
|
||||
|
||||
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
connection_ids.append(len(connection_ids) + 1)
|
||||
connection_id = connection_ids[-1]
|
||||
try:
|
||||
for setup_cmd in SETUP_COMMANDS:
|
||||
await reader.readexactly(len(setup_cmd))
|
||||
writer.write(_pack_frame(b""))
|
||||
await writer.drain()
|
||||
|
||||
await reader.readexactly(request_len)
|
||||
writer.write(_pack_frame(struct.pack("<H", 10 + connection_id)))
|
||||
await writer.drain()
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def main() -> None:
|
||||
server = await asyncio.start_server(handle, "127.0.0.1", 0)
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
try:
|
||||
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.2)
|
||||
first = await client.get_security_count(Market.SH)
|
||||
second = await client.get_security_count(Market.SH)
|
||||
await client.close()
|
||||
finally:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
assert first == 11
|
||||
assert second == 12
|
||||
assert len(connection_ids) == 2
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_async_client_request_timeout() -> None:
|
||||
request_len = len(GetSecurityCountCmd(Market.SH).build_request())
|
||||
|
||||
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
try:
|
||||
for setup_cmd in SETUP_COMMANDS:
|
||||
await reader.readexactly(len(setup_cmd))
|
||||
writer.write(_pack_frame(b""))
|
||||
await writer.drain()
|
||||
|
||||
await reader.readexactly(request_len)
|
||||
await asyncio.sleep(1.0)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def main() -> None:
|
||||
server = await asyncio.start_server(handle, "127.0.0.1", 0)
|
||||
port = server.sockets[0].getsockname()[1]
|
||||
try:
|
||||
client = AsyncTdxClient("127.0.0.1", port=port, timeout=0.05)
|
||||
await client.connect()
|
||||
t0 = time.monotonic()
|
||||
try:
|
||||
await client.get_security_count(Market.SH)
|
||||
except TdxConnectionError as exc:
|
||||
elapsed = time.monotonic() - t0
|
||||
assert "超时" in str(exc) or "timed out" in str(exc)
|
||||
assert elapsed < 0.3
|
||||
else: # pragma: no cover - 防御性断言
|
||||
raise AssertionError("expected timeout")
|
||||
finally:
|
||||
await client.close()
|
||||
finally:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,6 +1,5 @@
|
||||
"""get_price / put_price 单元测试,测试向量来自 pytdx 实际报文。"""
|
||||
|
||||
import pytest
|
||||
from xmtdx.codec.price import get_price, put_price
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ fixtures/ 目录下每个 .hex 文件是一次真实服务器响应的 body(
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import pytest
|
||||
|
||||
FIXTURES = pathlib.Path(__file__).parent.parent / "fixtures"
|
||||
|
||||
@@ -75,7 +74,7 @@ def test_security_list_gbk_no_crash():
|
||||
|
||||
def test_security_bars_parse():
|
||||
from xmtdx.commands.security_bars import GetSecurityBarsCmd
|
||||
from xmtdx.models.enums import Market, KlineCategory
|
||||
from xmtdx.models.enums import KlineCategory, Market
|
||||
|
||||
body = load_hex("security_bars")
|
||||
cmd = GetSecurityBarsCmd(Market.SH, "600000", KlineCategory.DAY, 0, 5)
|
||||
@@ -305,6 +304,7 @@ def test_company_info_category_parse():
|
||||
assert len(cats) == 16
|
||||
|
||||
c0 = cats[0]
|
||||
assert c0.name == "最新提示"
|
||||
assert c0.filename == "600000.txt"
|
||||
assert c0.start == 0
|
||||
assert c0.length == 11426
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""坏包与解码异常回归测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from xmtdx.codec.frame import FrameHeader, decompress_body
|
||||
from xmtdx.commands.company_info import GetCompanyInfoCategoryCmd
|
||||
from xmtdx.commands.security_count import GetSecurityCountCmd
|
||||
from xmtdx.commands.xdxr_info import GetXdxrInfoCmd
|
||||
from xmtdx.exceptions import TdxDecodeError
|
||||
from xmtdx.models.enums import Market
|
||||
|
||||
FIXTURES = Path(__file__).parent.parent / "fixtures"
|
||||
|
||||
|
||||
def _load_hex(name: str) -> bytes:
|
||||
return bytes.fromhex((FIXTURES / f"{name}.hex").read_text().strip())
|
||||
|
||||
|
||||
def test_security_count_truncated_raises_tdxdecodeerror() -> None:
|
||||
with pytest.raises(TdxDecodeError):
|
||||
GetSecurityCountCmd(Market.SH).parse_response(b"")
|
||||
|
||||
|
||||
def test_company_info_category_truncated_raises_tdxdecodeerror() -> None:
|
||||
body = _load_hex("company_info_category")
|
||||
cmd = GetCompanyInfoCategoryCmd(Market.SH, "600000")
|
||||
|
||||
with pytest.raises(TdxDecodeError):
|
||||
cmd.parse_response(body[:-10])
|
||||
|
||||
|
||||
def test_xdxr_info_truncated_raises_tdxdecodeerror() -> None:
|
||||
body = _load_hex("xdxr_info")
|
||||
cmd = GetXdxrInfoCmd(Market.SH, "600000")
|
||||
|
||||
with pytest.raises(TdxDecodeError):
|
||||
cmd.parse_response(body[:-10])
|
||||
|
||||
|
||||
def test_frame_bad_zlib_raises_tdxdecodeerror() -> None:
|
||||
with pytest.raises(TdxDecodeError):
|
||||
decompress_body(FrameHeader(0, 0, 0, 4, 8), b"xxxx")
|
||||
|
||||
|
||||
def test_frame_unzipsize_mismatch_raises_tdxdecodeerror() -> None:
|
||||
with pytest.raises(TdxDecodeError):
|
||||
decompress_body(FrameHeader(0, 0, 0, 3, 4), b"abc")
|
||||
Reference in New Issue
Block a user