using System.Reflection; using Fleet.Rpc; using Fleet.Server; using Microsoft.Extensions.DependencyInjection; using MoShiro.Probe; using MoShiro.Protocol; namespace MoShiro.Backend.Tests; /// /// 协议方向声明()和实际代码的对账:每个方法常量都声明了,每一侧该处理的方法都注册了处理函数, /// 二进制通道都有对应的收件箱,文档里写了每一个方法。新增方法绕不过声明,声明了没人处理也会被发现。 /// 注册处理函数时路由器本身还会核对方向、类型和角色,不符就在启动时失败。 /// public sealed class ProtocolDeclarationTests : IClassFixture { private static readonly MethodTable s_table = MoShiroMethodTable.Table; private readonly BackendFixture _backend; public ProtocolDeclarationTests(BackendFixture backend) { _backend = backend; } private static IEnumerable ConstantsOf(Type type) => type.GetFields(BindingFlags.Public | BindingFlags.Static).Where(f => f.IsLiteral && f.FieldType == typeof(string)).Select(f => (string)f.GetRawConstantValue()!); [Fact] public void EveryMethodConstant_IsDeclared() { HashSet declared = [.. s_table.Methods.Select(m => m.Name)]; string[] undeclared = [.. ConstantsOf(typeof(FleetMethods)).Concat(ConstantsOf(typeof(PeerMethods))).Concat(ConstantsOf(typeof(MoShiroMethods))).Where(n => !declared.Contains(n))]; Assert.Empty(undeclared); } [Fact] public void EveryDeclaredMethod_IsDocumented() { string root = FindRoot(); string docs = File.ReadAllText(Path.Combine(root, "docs", "design", "protocol.md")) + File.ReadAllText(Path.Combine(root, "backend", "fleet", "README.md")); string[] missing = [.. s_table.Methods.Select(m => m.Name).Distinct().Where(n => !docs.Contains($"`{n}`", StringComparison.Ordinal))]; Assert.Empty(missing); } [Fact] public void Backend_HandlesEveryMethodNodesMayInitiate_AndHasAnInboxForEveryDeclaredChannel() { FleetRouter router = _backend.Services.GetRequiredService(); Assert.Empty(router.Table.MissingHandlers(RpcSide.Server, null, router.Methods)); byte[] inboxes = [.. _backend.Services.GetServices().Select(i => i.Channel).Order()]; byte[] declared = [.. s_table.Channels.Where(c => c.Plane == RpcPlane.Node).Select(c => c.Channel).Order()]; Assert.Equal(declared, inboxes); } [Fact] public async Task Agent_HandlesEveryMethodItShould_OnBothPlanes() { await using AgentHarness agent = await AgentHarness.StartAsync(_backend); Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Agent, agent.App.Link.Router.Methods)); // 对等面:Agent 是监听方,要处理 Probe 发来的方法 Assert.Empty(s_table.MissingHandlers(RpcSide.Listener, null, agent.App.PeerRouter.Methods)); } [Fact] public async Task Probe_HandlesEveryMethodItShould_OnBothPlanes() { await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend); Assert.Empty(s_table.MissingHandlers(RpcSide.Node, NodeRoles.Probe, probe.App.Link.Router.Methods)); // 对等面:Probe 是拨号方,要处理 Agent 发来的方法 Assert.Empty(s_table.MissingHandlers(RpcSide.Dialer, null, AgentConnection.Router.Methods)); } [Fact] public void RolesOfNodeIds_FollowThePrefix() { Assert.Equal(NodeRoles.Agent, NodeRoles.Of(AgentIds.Prefix + "_01ABC")); Assert.Equal(NodeRoles.Probe, NodeRoles.Of(ProbeId.Prefix + "_01ABC")); Assert.Null(NodeRoles.Of("usr_01ABC")); } private static string FindRoot() { string? dir = AppContext.BaseDirectory; while (dir is not null && !File.Exists(Path.Combine(dir, "docs", "design", "protocol.md"))) { dir = Path.GetDirectoryName(dir); } return dir ?? throw new InvalidOperationException("repository root not found"); } }