From 7456f81c800736515b2d143316ebe08eec524d37 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Sat, 5 Sep 2026 03:30:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9D=BF=E5=9D=97=E6=88=90=E5=88=86?= =?UTF-8?q?=E8=82=A1/=E6=8E=92=E8=A1=8C=E6=8A=A5=E4=BB=B7=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E5=90=88=E5=B9=B6=E9=A1=BA=E5=BA=8F=E9=A2=A0=E5=80=92?= =?UTF-8?q?=20=E2=80=94=20=E5=A4=9A=E9=A1=B5=E5=89=8D=E6=8F=92=E6=94=B9?= =?UTF-8?q?=E6=8C=89=E9=A1=B5=E5=BA=8F=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BoardMembersQuotesCmd 分页合并曾用 batch + all_quotes 前插,而协议 start=0 返回排序后最前一页:成分股超过单页 80 只的板块(半导体 SH881319、 光学光电 SH881329、通信设备 SH881338 等)弹窗涨跌幅榜从第 81~100 名开头, 前 80 名被压到后面,肉眼即乱序;≤80 只的板块单页不受影响。 同步/异步 get_board_members 与 get_stock_quotes_list 共 4 处统一改为 按页序追加(all_quotes = all_quotes + batch),回归测试钉死多页合并顺序、 start 偏移推进与末页不足一页的终止行为。 注:test_ex_reconnect::test_all_retries_relogin_each_time 存量失败 (干净工作树同样失败),与本修复无关,另案处理。 --- src/easy_tdx/mac/client.py | 8 +- tests/unit/test_board_members_pagination.py | 103 ++++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_board_members_pagination.py diff --git a/src/easy_tdx/mac/client.py b/src/easy_tdx/mac/client.py index bd78554..9ad1a7e 100644 --- a/src/easy_tdx/mac/client.py +++ b/src/easy_tdx/mac/client.py @@ -388,7 +388,7 @@ class MacClient: ) if not batch: break - all_quotes = batch + all_quotes + all_quotes = all_quotes + batch fetched += len(batch) offset += len(batch) if len(batch) < page_size: @@ -799,7 +799,7 @@ class MacClient: ) if not batch: break - all_quotes = batch + all_quotes + all_quotes = all_quotes + batch fetched += len(batch) offset += len(batch) if len(batch) < page_size: @@ -1448,7 +1448,7 @@ class AsyncMacClient(AsyncHeartbeatMixin): ) if not batch: break - all_quotes = batch + all_quotes + all_quotes = all_quotes + batch fetched += len(batch) offset += len(batch) if len(batch) < page_size: @@ -1787,7 +1787,7 @@ class AsyncMacClient(AsyncHeartbeatMixin): ) if not batch: break - all_quotes = batch + all_quotes + all_quotes = all_quotes + batch fetched += len(batch) offset += len(batch) if len(batch) < page_size: diff --git a/tests/unit/test_board_members_pagination.py b/tests/unit/test_board_members_pagination.py new file mode 100644 index 0000000..e7cfe39 --- /dev/null +++ b/tests/unit/test_board_members_pagination.py @@ -0,0 +1,103 @@ +"""成分股/排行报价分页合并顺序回归测试。 + +真实 bug(v1.32.x):BoardMembersQuotesCmd 分页合并曾用 ``batch + all_quotes`` +前插,而协议 ``start=0`` 返回排序后**最前**的一页——多页时整表被按页倒序拼接: +成分股超过单页 80 只的板块(如半导体 SH881319),弹窗涨跌幅榜从第 81 名开头, +前 80 名被压到后面,肉眼即"乱序"。 + +本测试钉死:多页合并必须按请求顺序追加,且 start 偏移正确推进。 +""" + +from unittest.mock import patch + +import pytest + +from easy_tdx.mac.client import AsyncMacClient, MacClient +from easy_tdx.mac.enums import SortOrder, SortType +from easy_tdx.mac.models import MacQuoteField + +_TOTAL = 100 # 超过单页 80,触发两页 + + +def _make_rows(start: int, n: int) -> list[MacQuoteField]: + """模拟服务器:按涨跌幅降序返回第 start..start+n 名。""" + + def code(i: int) -> str: + return f"{600000 + i:06d}" + + return [ + MacQuoteField( + market=1, + code=code(i), + name=f"股{i}", + fields={"change_pct": 10.0 - i * 0.2}, + ) + for i in range(start, start + n) + ] + + +def _fake_execute_factory(seen: list): + def fake_execute(cmd): + seen.append(cmd) + # 真实服务器按剩余行数返回(末页不足 page_size),模拟之 + n = min(cmd._page_size, _TOTAL - cmd._start) + return _make_rows(cmd._start, n) + + return fake_execute + + +def test_board_members_pages_appended_in_order(): + """get_board_members 多页合并按页序追加:第 0 行=第 1 名,第 80 行=第 81 名。""" + client = MacClient.__new__(MacClient) + seen: list = [] + + with patch.object(client, "_execute", side_effect=_fake_execute_factory(seen)): + df = client.get_board_members( + "881319", + count=_TOTAL, + sort_type=SortType.CHANGE_PCT, + sort_order=SortOrder.DESC, + ) + + assert len(df) == _TOTAL + assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)] + # 两页:start 0(80 行)→ start 80(20 行) + assert [c._start for c in seen] == [0, 80] + + +@pytest.mark.asyncio +async def test_board_members_async_pages_appended_in_order(): + client = AsyncMacClient.__new__(AsyncMacClient) + seen: list = [] + + async def fake_execute(cmd): + seen.append(cmd) + return _make_rows(cmd._start, cmd._page_size) + + with patch.object(client, "_execute", side_effect=fake_execute): + df = await client.get_board_members( + "881319", + count=_TOTAL, + sort_type=SortType.CHANGE_PCT, + sort_order=SortOrder.DESC, + ) + + assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)] + assert [c._start for c in seen] == [0, 80] + + +def test_quotes_list_pages_appended_in_order(): + """get_stock_quotes_list(看板涨跌榜数据源)同样按页序追加。""" + client = MacClient.__new__(MacClient) + seen: list = [] + + with patch.object(client, "_execute", side_effect=_fake_execute_factory(seen)): + df = client.get_stock_quotes_list( + category=1, # Category.A + count=_TOTAL, + sort_type=SortType.CHANGE_PCT, + sort_order=SortOrder.DESC, + ) + + assert df["code"].tolist() == [f"{600000 + i:06d}" for i in range(_TOTAL)] + assert [c._start for c in seen] == [0, 80]