ci/woodpecker/push/main-amd64-build Pipeline was successful
Methods, roles and binary channels are declared in one table per plane (node and peer). RpcRouter checks direction, kind and role when handlers are registered, when messages arrive (fleet_protocol_violation, disconnect after repeated violations) and before sending. NodeConnection is split by responsibility.
95 lines
4.0 KiB
C#
95 lines
4.0 KiB
C#
using System.Reflection;
|
|
using Fleet.Rpc;
|
|
using Fleet.Server;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using MoShiro.Probe;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
/// <summary>
|
|
/// 协议方向声明(<see cref="MoShiroMethodTable"/>)和实际代码的对账:每个方法常量都声明了,每一侧该处理的方法都注册了处理函数,
|
|
/// 二进制通道都有对应的收件箱,文档里写了每一个方法。新增方法绕不过声明,声明了没人处理也会被发现。
|
|
/// 注册处理函数时路由器本身还会核对方向、类型和角色,不符就在启动时失败。
|
|
/// </summary>
|
|
[Collection("probe-e2e")]
|
|
public sealed class ProtocolDeclarationTests
|
|
{
|
|
private static readonly MethodTable s_table = MoShiroMethodTable.Table;
|
|
|
|
private readonly BackendFixture _backend;
|
|
|
|
public ProtocolDeclarationTests(BackendFixture backend)
|
|
{
|
|
_backend = backend;
|
|
}
|
|
|
|
private static IEnumerable<string> 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<string> 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", "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<FleetRouter>();
|
|
Assert.Empty(router.Table.MissingHandlers(RpcSide.Server, null, router.Methods));
|
|
|
|
byte[] inboxes = [.. _backend.Services.GetServices<IInbox>().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", "protocol.md")))
|
|
{
|
|
dir = Path.GetDirectoryName(dir);
|
|
}
|
|
|
|
return dir ?? throw new InvalidOperationException("repository root not found");
|
|
}
|
|
}
|