Files
AIC-Project/Server/Server.Host.Tests/RoomTests.cs
T
2026-07-10 10:24:29 +08:00

187 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using Xunit;
using XWorld.Framework;
using XWorld.Server.Host;
namespace XWorld.Server.Host.Tests
{
public class RoomTests
{
private sealed class FakeOutput : IRoomOutput
{
public int Broadcasts;
public void Broadcast(NetMessage m) => Broadcasts++;
public void SendTo(int playerId, NetMessage m) { }
}
// 正常房间:第 N 次 tick 时通过 ctx.EndRoom 请求结束
private sealed class CountingRoom : IGameServerRoom
{
private IRoomCtx _ctx;
private int _ticks;
public int EndCalls;
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) => _ctx = ctx;
public void OnMessage(int playerId, NetMessage message) { }
public void OnTick(float dt)
{
_ticks++;
_ctx.Broadcast(new NetMessage(1, new byte[0]));
if (_ticks >= 2) _ctx.EndRoom();
}
public void OnRoomEnd() => EndCalls++;
}
// 故障房间:OnTick 抛异常
private sealed class ThrowingRoom : IGameServerRoom
{
public int EndCalls;
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) { }
public void OnMessage(int playerId, NetMessage message) { }
public void OnTick(float dt) => throw new InvalidOperationException("boom");
public void OnRoomEnd() => EndCalls++;
}
private static (Room room, RoomCtx ctx, T logic) Build<T>(string id, T logic) where T : IGameServerRoom
{
var clock = new ManualClock();
Room room = null;
var ctx = new RoomCtx(new DeterministicRandom(1), clock, new ConsoleLogger(),
new InMemoryStorage(), new FakeOutput(), r => room.RequestEnd(r));
room = new Room(id, logic, clock, ctx, new ConsoleLogger());
return (room, ctx, logic);
}
private static readonly IReadOnlyList<PlayerInfo> Players =
new[] { new PlayerInfo { PlayerId = 1, Name = "P", IsAI = false } };
[Fact]
public void NormalRoom_EndsAfterRequest_AndCallsOnRoomEndOnce()
{
var (room, _, logic) = Build("r1", new CountingRoom());
room.Start(Players);
Assert.True(room.IsActive);
room.Tick(1f); // _ticks=1
Assert.True(room.IsActive);
room.Tick(1f); // _ticks=2 → EndRoom 请求 → 本 tick 后结束
Assert.False(room.IsActive);
Assert.Equal(1, logic.EndCalls);
room.Tick(1f); // 已结束,再 tick 无效
Assert.Equal(1, logic.EndCalls);
}
[Fact]
public void ThrowingRoom_FaultsOut_Safely_WithoutPropagating()
{
var (room, _, logic) = Build("r2", new ThrowingRoom());
room.Start(Players);
var ex = Record.Exception(() => room.Tick(1f)); // 不应向外抛
Assert.Null(ex);
Assert.False(room.IsActive);
Assert.True(room.FaultedOut);
Assert.Equal(1, logic.EndCalls); // 故障也走一次安全结束
}
[Fact]
public void ClockAdvances_BeforeOnTick()
{
float seen = -1f;
var capture = new ClockProbeRoom(now => seen = now);
var (room, _, _) = Build("r3", capture);
room.Start(Players);
room.Tick(2f);
Assert.Equal(2f, seen); // OnTick 内可见已推进的时钟
}
// 在 OnRoomStart / OnMessage 抛异常的故障房间
private sealed class ThrowOnStartRoom : IGameServerRoom
{
public int EndCalls;
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) => throw new InvalidOperationException("start boom");
public void OnMessage(int playerId, NetMessage message) { }
public void OnTick(float dt) { }
public void OnRoomEnd() => EndCalls++;
}
private sealed class ThrowOnMessageRoom : IGameServerRoom
{
public int EndCalls;
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) { }
public void OnMessage(int playerId, NetMessage message) => throw new InvalidOperationException("msg boom");
public void OnTick(float dt) { }
public void OnRoomEnd() => EndCalls++;
}
[Fact]
public void ThrowOnStart_FaultsOut_Safely()
{
var (room, _, logic) = Build("rs", new ThrowOnStartRoom());
var ex = Record.Exception(() => room.Start(Players));
Assert.Null(ex); // 不向宿主外抛
Assert.False(room.IsActive);
Assert.True(room.FaultedOut);
Assert.Equal(1, logic.EndCalls); // 安全结束一次
}
[Fact]
public void ThrowOnMessage_FaultsOut_Safely()
{
var (room, _, logic) = Build("rm", new ThrowOnMessageRoom());
room.Start(Players);
Assert.True(room.IsActive);
var ex = Record.Exception(() => room.Deliver(1, new NetMessage(0, new byte[0])));
Assert.Null(ex);
Assert.False(room.IsActive);
Assert.True(room.FaultedOut);
Assert.Equal(1, logic.EndCalls);
}
// 结束时带结算结果的房间
private sealed class ResultRoom : IGameServerRoom
{
public RoomEndResult Result;
private IRoomCtx _ctx;
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) => _ctx = ctx;
public void OnMessage(int playerId, NetMessage message) { }
public void OnTick(float dt) => _ctx.EndRoom(Result);
public void OnRoomEnd() { }
}
[Fact]
public void EndRoomWithResult_IsCapturedOnRoom()
{
var result = new RoomEndResult { WinnerPlayerId = 7, ResultBlob = new byte[] { 9 } };
var (room, _, _) = Build("rr", new ResultRoom { Result = result });
room.Start(Players);
room.Tick(1f);
Assert.False(room.IsActive);
Assert.Same(result, room.EndResult);
}
[Fact]
public void EndRoomWithoutResult_EndResultIsNull()
{
var (room, _, _) = Build("rn", new CountingRoom()); // CountingRoom 调无参 EndRoom
room.Start(Players);
room.Tick(1f);
room.Tick(1f); // 第 2 tick 触发 EndRoom()
Assert.False(room.IsActive);
Assert.Null(room.EndResult);
}
private sealed class ClockProbeRoom : IGameServerRoom
{
private readonly Action<float> _probe;
private IRoomCtx _ctx;
public ClockProbeRoom(Action<float> probe) { _probe = probe; }
public void OnRoomStart(IReadOnlyList<PlayerInfo> players, IRoomCtx ctx) => _ctx = ctx;
public void OnMessage(int playerId, NetMessage message) { }
public void OnTick(float dt) => _probe(_ctx.Timer.Now);
public void OnRoomEnd() { }
}
}
}