diff --git a/danding_bot/plugins/group_horse_racing/test_commands.py b/danding_bot/plugins/group_horse_racing/test_commands.py index fb6a69b..56cba00 100644 --- a/danding_bot/plugins/group_horse_racing/test_commands.py +++ b/danding_bot/plugins/group_horse_racing/test_commands.py @@ -8,6 +8,7 @@ from .models import Horse, HorseState, RoomState, Bet, RaceResult import asyncio import random from datetime import datetime +import traceback from . import commands as commands_mod @@ -205,98 +206,116 @@ async def handle_test_simulate_race(bot: Bot, event: Event): scope = get_scope(event) lock = room_store.get_lock(scope) + try: + await asyncio.wait_for(lock.acquire(), timeout=3) + except TimeoutError: + await test_simulate_race_cmd.finish("完全模拟失败:房间锁被占用(可能有卡住的赛马/测试任务),请稍后重试或先重启 Bot") + return - async with lock: + try: race_engine.stop_race(scope) room_store.delete_room(scope) + finally: + lock.release() - 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 + 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 - fake_room_store = _InMemoryRoomStore() - fake_points_service = _InMemoryPointsService() - fake_message_service = _NoopMessageService() - fake_bot = _FakeBot() + fake_room_store = _InMemoryRoomStore() + fake_points_service = _InMemoryPointsService() + fake_message_service = _NoopMessageService() + fake_bot = _FakeBot() - 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 + start_task: asyncio.Task | None = None + room = None - 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) + 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 - 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)) + 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) - room.state = RoomState.WAITING + 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)) - for horse in room.horses.values(): - horse.state = HorseState.RACING + room.state = RoomState.WAITING - 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 + for horse in room.horses.values(): + horse.state = HorseState.RACING - messages = [m.get("message", "") for m in fake_bot.messages] - if not messages: - await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息") - 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 asyncio.wait_for(start_task, timeout=15) - if "比赛开始!" not in messages[0]: - await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息") - return + messages = [m.get("message", "") for m in fake_bot.messages] + if not messages: + 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 + if "比赛开始!" not in messages[0]: + 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 any("【第" in msg and "回合】" in msg for msg in messages): + await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息") + return - if not fake_room_store.saved_results: - await test_simulate_race_cmd.finish("完全模拟失败:未写入赛史结果(内存)") - return + result_msg = messages[-1] + if "比赛结束!冠军:" not in result_msg: + 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 + if not fake_room_store.saved_results: + 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 + saved = fake_room_store.saved_results[-1] + if saved.champion_name not in room.horses: + 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 + 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 - 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)}(冠军/参赛/下注派奖)", - ] - ) + 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 + ) + except asyncio.TimeoutError: + ticks = room.tick_count if room else 0 + await test_simulate_race_cmd.finish(f"完全模拟失败:超时未完成(当前回合:{ticks})") + except Exception as e: + tail = "\n".join(traceback.format_exc().splitlines()[-8:]) + await test_simulate_race_cmd.finish(f"完全模拟异常:{type(e).__name__}: {e}\n{tail}") + finally: + if start_task and not start_task.done(): + start_task.cancel() + 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