From 1c9d9647474ff95591a49770b978de363988b4f8 Mon Sep 17 00:00:00 2001 From: "Mr.Xia" <1424473282@qq.com> Date: Sat, 4 Apr 2026 21:21:36 +0800 Subject: [PATCH] =?UTF-8?q?test(group=5Fhorse=5Fracing):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E6=A8=A1=E6=8B=9F=E8=B5=9B=E9=A9=AC=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=BB=A5=E4=BD=BF=E7=94=A8=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构测试命令 `测试模拟赛马`,使用模拟依赖(内存存储、积分服务)替换真实组件,避免测试产生数据库副作用。 新增模拟类 `_FakeBot`、`_InMemoryRoomStore`、`_InMemoryPointsService` 和 `_NoopMessageService`,并临时替换模块中的全局依赖以执行完整比赛流程。 验证开赛消息、回合进度、结束结算、赛史保存及积分奖励调用的正确性,确保核心逻辑在隔离环境中正常工作。 --- .../group_horse_racing/test_commands.py | 198 +++++++++++++----- 1 file changed, 145 insertions(+), 53 deletions(-) diff --git a/danding_bot/plugins/group_horse_racing/test_commands.py b/danding_bot/plugins/group_horse_racing/test_commands.py index 79f8cd5..570c314 100644 --- a/danding_bot/plugins/group_horse_racing/test_commands.py +++ b/danding_bot/plugins/group_horse_racing/test_commands.py @@ -3,9 +3,13 @@ from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMe from . import plugin_config as config from .commands import get_scope, check_access, room_store, points_service, race_engine -from .models import Horse, HorseState, RoomState +from .models import Horse, HorseState, RoomState, Bet, RaceResult +import asyncio import random +from datetime import datetime + +from . import commands as commands_mod async def check_tester(event: Event) -> bool: @@ -132,6 +136,65 @@ def _generate_random_horse_names(count: int) -> list[str]: test_simulate_race_cmd = on_command("测试模拟赛马", priority=5) +class _FakeBot: + def __init__(self): + self.messages: list[dict] = [] + self._next_message_id = 1 + + async def send_msg(self, **kwargs): + self.messages.append(dict(kwargs)) + message_id = str(self._next_message_id) + self._next_message_id += 1 + return message_id + + +class _InMemoryRoomStore: + def __init__(self): + self.rooms: dict[str, "commands_mod.Room"] = {} + self.saved_results: list[RaceResult] = [] + + def get_room(self, scope: str): + return self.rooms.get(scope) + + def create_room(self, scope: str): + room = commands_mod.Room(scope=scope) + self.rooms[scope] = room + return room + + def delete_room(self, scope: str): + if scope in self.rooms: + del self.rooms[scope] + + def save_race_result(self, result: RaceResult): + self.saved_results.append(result) + + +class _InMemoryPointsService: + def __init__(self): + self.calls: list[tuple[str, dict]] = [] + + async def reward_champion(self, user_id: str): + self.calls.append(("reward_champion", {"user_id": user_id})) + return True, 0 + + async def reward_participant(self, user_id: str): + self.calls.append(("reward_participant", {"user_id": user_id})) + return True, 0 + + async def payout_winnings(self, user_id: str, amount: int, odds: float): + self.calls.append(("payout_winnings", {"user_id": user_id, "amount": amount, "odds": odds})) + return True, 0 + + async def refund_bet_points(self, user_id: str, amount: int, reason: str = "比赛中断退还"): + self.calls.append(("refund_bet_points", {"user_id": user_id, "amount": amount, "reason": reason})) + return True, 0 + + +class _NoopMessageService: + def clear_pending_recalls(self, scope: str): + return + + @test_simulate_race_cmd.handle() async def handle_test_simulate_race(bot: Bot, event: Event): if not await check_tester(event): @@ -144,65 +207,94 @@ async def handle_test_simulate_race(bot: Bot, event: Event): async with lock: race_engine.stop_race(scope) room_store.delete_room(scope) - room = room_store.create_room(scope) - horse_names = _generate_random_horse_names(8) - for idx, horse_name in enumerate(horse_names, start=1): - owner_id = f"sim_user_{idx}" - room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING) + original_room_store = commands_mod.room_store + original_points_service = commands_mod.points_service + original_message_service = commands_mod.message_service + original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL - room.state = RoomState.RUNNING + fake_room_store = _InMemoryRoomStore() + fake_points_service = _InMemoryPointsService() + fake_message_service = _NoopMessageService() + fake_bot = _FakeBot() - progress_samples: list[str] = [] - last_progress = "" - finished = [] + try: + commands_mod.room_store = fake_room_store + commands_mod.points_service = fake_points_service + commands_mod.message_service = fake_message_service + commands_mod.config.RACE_TICK_INTERVAL = 0 - for _ in range(1_000): - finished = race_engine.tick(room) - last_progress = race_engine.format_progress(room) - if len(progress_samples) < 3: - progress_samples.append(last_progress) - if finished: - break + room = fake_room_store.create_room(scope) + horse_names = _generate_random_horse_names(8) + for idx, horse_name in enumerate(horse_names, start=1): + owner_id = f"sim_user_{idx}" + room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING) - if not finished: - race_engine.stop_race(scope) - room_store.delete_room(scope) - await test_simulate_race_cmd.finish("模拟失败:超过最大回合仍未结束") - return + bet_amount = max(commands_mod.config.MIN_BET, 10) + room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount)) + room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2)) - champion = race_engine.determine_champion(finished) - room.champion_name = champion.name - room.state = RoomState.FINISHED + room.state = RoomState.WAITING - expected_lines = 1 + len(room.horses) - actual_lines = len(last_progress.splitlines()) - if actual_lines != expected_lines: - race_engine.stop_race(scope) - room_store.delete_room(scope) - await test_simulate_race_cmd.finish(f"模拟失败:进度渲染行数不匹配(期望{expected_lines},实际{actual_lines})") - return + for horse in room.horses.values(): + horse.state = HorseState.RACING - missing_names = [name for name in room.horses.keys() if name not in last_progress] - if missing_names: - race_engine.stop_race(scope) - room_store.delete_room(scope) - await test_simulate_race_cmd.finish(f"模拟失败:进度渲染缺少马名:{', '.join(missing_names)}") - return + start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope)) + commands_mod.race_engine.register_task(scope, start_task) + await start_task - race_engine.stop_race(scope) - room_store.delete_room(scope) + messages = [m.get("message", "") for m in fake_bot.messages] + if not messages: + await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息") + return - sample_block = "\n\n".join(progress_samples[-2:] + [last_progress] if len(progress_samples) >= 2 else [last_progress]) - await test_simulate_race_cmd.finish( - "\n".join( - [ - "模拟赛马完成", - f"参赛马匹:{', '.join(horse_names)}", - f"冠军:{room.champion_name}", - f"总回合:{room.tick_count}", - "进度片段:", - sample_block, - ] - ) - ) + if "比赛开始!" not in messages[0]: + await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息") + return + + if not any("【第" in msg and "回合】" in msg for msg in messages): + await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息") + return + + result_msg = messages[-1] + if "比赛结束!冠军:" not in result_msg: + await test_simulate_race_cmd.finish("完全模拟失败:未发送结束结算消息") + return + + if not fake_room_store.saved_results: + await test_simulate_race_cmd.finish("完全模拟失败:未写入赛史结果(内存)") + return + + saved = fake_room_store.saved_results[-1] + if saved.champion_name not in room.horses: + await test_simulate_race_cmd.finish("完全模拟失败:赛史冠军不在参赛马匹中") + return + + champion_owner_id = room.horses[saved.champion_name].owner_id + reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"] + if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id: + await test_simulate_race_cmd.finish("完全模拟失败:未正确发放冠军奖励(内存记录)") + return + + participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"] + if len(participant_calls) != max(0, len(room.horses) - 1): + await test_simulate_race_cmd.finish("完全模拟失败:参赛奖励次数不匹配(内存记录)") + return + + await test_simulate_race_cmd.finish( + "\n".join( + [ + "完全模拟赛马完成(无真实积分/数据库副作用)", + f"参赛马匹:{', '.join(horse_names)}", + f"冠军:{saved.champion_name}(马主:{saved.champion_owner})", + f"总回合:{saved.duration_ticks}", + f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)", + f"结算调用:{len(fake_points_service.calls)}(冠军/参赛/下注派奖)", + ] + ) + ) + finally: + commands_mod.room_store = original_room_store + commands_mod.points_service = original_points_service + commands_mod.message_service = original_message_service + commands_mod.config.RACE_TICK_INTERVAL = original_tick_interval