fix(test): 修复完全模拟赛马测试的并发与超时问题

- 将锁获取改为带超时的等待,避免因锁占用导致测试卡死
- 增加模拟任务的超时控制,防止无限等待
- 添加异常捕获与详细错误信息输出,便于问题定位
- 确保在测试异常或超时后正确清理资源
This commit is contained in:
2026-04-04 21:36:58 +08:00
parent e22b44ff07
commit e5d0db268b

View File

@@ -8,6 +8,7 @@ from .models import Horse, HorseState, RoomState, Bet, RaceResult
import asyncio import asyncio
import random import random
from datetime import datetime from datetime import datetime
import traceback
from . import commands as commands_mod from . import commands as commands_mod
@@ -205,98 +206,116 @@ async def handle_test_simulate_race(bot: Bot, event: Event):
scope = get_scope(event) scope = get_scope(event)
lock = room_store.get_lock(scope) 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) race_engine.stop_race(scope)
room_store.delete_room(scope) room_store.delete_room(scope)
finally:
lock.release()
original_room_store = commands_mod.room_store original_room_store = commands_mod.room_store
original_points_service = commands_mod.points_service original_points_service = commands_mod.points_service
original_message_service = commands_mod.message_service original_message_service = commands_mod.message_service
original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL
fake_room_store = _InMemoryRoomStore() fake_room_store = _InMemoryRoomStore()
fake_points_service = _InMemoryPointsService() fake_points_service = _InMemoryPointsService()
fake_message_service = _NoopMessageService() fake_message_service = _NoopMessageService()
fake_bot = _FakeBot() fake_bot = _FakeBot()
try: start_task: asyncio.Task | None = None
commands_mod.room_store = fake_room_store room = None
commands_mod.points_service = fake_points_service
commands_mod.message_service = fake_message_service
commands_mod.config.RACE_TICK_INTERVAL = 0
room = fake_room_store.create_room(scope) try:
horse_names = _generate_random_horse_names(8) commands_mod.room_store = fake_room_store
for idx, horse_name in enumerate(horse_names, start=1): commands_mod.points_service = fake_points_service
owner_id = f"sim_user_{idx}" commands_mod.message_service = fake_message_service
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING) commands_mod.config.RACE_TICK_INTERVAL = 0
bet_amount = max(commands_mod.config.MIN_BET, 10) room = fake_room_store.create_room(scope)
room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount)) horse_names = _generate_random_horse_names(8)
room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2)) 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(): room.state = RoomState.WAITING
horse.state = HorseState.RACING
start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope)) for horse in room.horses.values():
commands_mod.race_engine.register_task(scope, start_task) horse.state = HorseState.RACING
await start_task
messages = [m.get("message", "") for m in fake_bot.messages] start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
if not messages: commands_mod.race_engine.register_task(scope, start_task)
await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息") await asyncio.wait_for(start_task, timeout=15)
return
if "比赛开始!" not in messages[0]: messages = [m.get("message", "") for m in fake_bot.messages]
await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息") if not messages:
return await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息")
return
if not any("【第" in msg and "回合】" in msg for msg in messages): if "比赛开始!" not in messages[0]:
await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息") await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息")
return return
result_msg = messages[-1] if not any("【第" in msg and "回合】" in msg for msg in messages):
if "比赛结束!冠军:" not in result_msg: await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息")
await test_simulate_race_cmd.finish("完全模拟失败:未发送结束结算消息") return
return
if not fake_room_store.saved_results: result_msg = messages[-1]
await test_simulate_race_cmd.finish("完全模拟失败:未写入赛史结果(内存)") if "比赛结束!冠军:" not in result_msg:
return await test_simulate_race_cmd.finish("完全模拟失败:未发送结束结算消息")
return
saved = fake_room_store.saved_results[-1] if not fake_room_store.saved_results:
if saved.champion_name not in room.horses: await test_simulate_race_cmd.finish("完全模拟失败:未写入赛史结果(内存)")
await test_simulate_race_cmd.finish("完全模拟失败:赛史冠军不在参赛马匹中") return
return
champion_owner_id = room.horses[saved.champion_name].owner_id saved = fake_room_store.saved_results[-1]
reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"] if saved.champion_name not in room.horses:
if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id: await test_simulate_race_cmd.finish("完全模拟失败:赛史冠军不在参赛马匹中")
await test_simulate_race_cmd.finish("完全模拟失败:未正确发放冠军奖励(内存记录)") return
return
participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"] champion_owner_id = room.horses[saved.champion_name].owner_id
if len(participant_calls) != max(0, len(room.horses) - 1): reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"]
await test_simulate_race_cmd.finish("完全模拟失败:参赛奖励次数不匹配(内存记录)") if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id:
return await test_simulate_race_cmd.finish("完全模拟失败:未正确发放冠军奖励(内存记录)")
return
await test_simulate_race_cmd.finish( participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"]
"\n".join( if len(participant_calls) != max(0, len(room.horses) - 1):
[ await test_simulate_race_cmd.finish("完全模拟失败:参赛奖励次数不匹配(内存记录)")
"完全模拟赛马完成(无真实积分/数据库副作用)", return
f"参赛马匹:{', '.join(horse_names)}",
f"冠军:{saved.champion_name}(马主:{saved.champion_owner}", await test_simulate_race_cmd.finish(
f"总回合:{saved.duration_ticks}", "\n".join(
f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)", [
f"结算调用:{len(fake_points_service.calls)}(冠军/参赛/下注派奖", "完全模拟赛马完成(无真实积分/数据库副作用",
] 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 except asyncio.TimeoutError:
commands_mod.points_service = original_points_service ticks = room.tick_count if room else 0
commands_mod.message_service = original_message_service await test_simulate_race_cmd.finish(f"完全模拟失败:超时未完成(当前回合:{ticks}")
commands_mod.config.RACE_TICK_INTERVAL = original_tick_interval 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