187 lines
7.2 KiB
C#
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() { }
|
|
}
|
|
}
|
|
}
|