132 lines
5.4 KiB
C#
132 lines
5.4 KiB
C#
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Protocol.Tests;
|
|
|
|
public sealed class CatalogTests
|
|
{
|
|
private static JsonElement Json(string text) => JsonDocument.Parse(text).RootElement.Clone();
|
|
|
|
[Fact]
|
|
public void EveryKind_HasBoundedParams_ValidDefaults_AndUniqueNames()
|
|
{
|
|
Assert.Equal(Kinds.All.Count, Kinds.All.Select(k => k.Id).Distinct().Count());
|
|
foreach (KindSpec kind in Kinds.All)
|
|
{
|
|
ParamResolution defaults = ParamResolver.Resolve(kind.Params, null);
|
|
Assert.True(defaults.IsValid, $"{kind.Id}: defaults must be valid");
|
|
Assert.Equal(kind.Params.Count, kind.Params.Select(p => p.Name).Distinct().Count());
|
|
Assert.True(kind.BudgetBitsPerSecond(defaults.Values!) >= 0);
|
|
foreach (ParamSpec spec in kind.Params)
|
|
{
|
|
// 每个参数的默认值必须能通过自己的校验(上下限在构造时已写明)
|
|
Assert.True(spec.TryNormalize(spec.DefaultValue, out _, out string error), $"{kind.Id}.{spec.Name}: {error}");
|
|
}
|
|
|
|
foreach (StreamSpec stream in kind.Streams)
|
|
{
|
|
Assert.Equal(stream.Columns.Count, stream.Columns.Select(c => c.Name).Distinct().Count());
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultLineTemplates_StayWithinTheLineBudget()
|
|
{
|
|
long total = 0;
|
|
foreach (KindSpec kind in new[] { Kinds.QuicPing, Kinds.TcpPing })
|
|
{
|
|
total += kind.BudgetBitsPerSecond(ParamResolver.Resolve(kind.Params, null).Values!);
|
|
}
|
|
|
|
Assert.InRange(total, 1, ProtocolLimits.MaxLineBudgetBitsPerSecond);
|
|
}
|
|
|
|
[Fact]
|
|
public void Resolve_FillsDefaults_AndReportsEveryProblem()
|
|
{
|
|
var input = new Dictionary<string, JsonElement>
|
|
{
|
|
["rate_hz"] = Json("500"),
|
|
["size"] = Json("\"big\""),
|
|
["bogus"] = Json("1"),
|
|
};
|
|
ParamResolution result = ParamResolver.Resolve(Kinds.QuicPing.Params, input);
|
|
|
|
Assert.False(result.IsValid);
|
|
Assert.Contains("between 1 and 200", result.Errors["rate_hz"]);
|
|
Assert.Equal("must be an integer", result.Errors["size"]);
|
|
Assert.Contains("not a parameter", result.Errors["bogus"]);
|
|
Assert.False(result.Errors.ContainsKey("port"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Resolve_AcceptsBoundariesAndRejectsJustOutside()
|
|
{
|
|
Assert.True(ParamResolver.Resolve(Kinds.QuicPing.Params, new Dictionary<string, JsonElement> { ["rate_hz"] = Json("1") }).IsValid);
|
|
Assert.True(ParamResolver.Resolve(Kinds.QuicPing.Params, new Dictionary<string, JsonElement> { ["rate_hz"] = Json("200") }).IsValid);
|
|
Assert.False(ParamResolver.Resolve(Kinds.QuicPing.Params, new Dictionary<string, JsonElement> { ["rate_hz"] = Json("0") }).IsValid);
|
|
Assert.False(ParamResolver.Resolve(Kinds.QuicPing.Params, new Dictionary<string, JsonElement> { ["rate_hz"] = Json("201") }).IsValid);
|
|
Assert.False(ParamResolver.Resolve(Kinds.QuicPing.Params, new Dictionary<string, JsonElement> { ["rate_hz"] = Json("1.5") }).IsValid);
|
|
Assert.False(ParamResolver.Resolve(Kinds.QuicStream.Params, new Dictionary<string, JsonElement> { ["direction"] = Json("\"sideways\"") }).IsValid);
|
|
Assert.False(ParamResolver.Resolve(Kinds.TcpPing.Params, new Dictionary<string, JsonElement> { ["tls"] = Json("1") }).IsValid);
|
|
}
|
|
|
|
[Fact]
|
|
public void Kinds_AreUnknownWhenNotDeclared()
|
|
{
|
|
Assert.Null(Kinds.Find("nope"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("continuous", true)]
|
|
[InlineData("once", true)]
|
|
[InlineData("every:60", true)]
|
|
[InlineData("every:59", false)]
|
|
[InlineData("every:", false)]
|
|
[InlineData("every:-5", false)]
|
|
[InlineData("hourly", false)]
|
|
public void Schedule_Validation(string schedule, bool valid)
|
|
{
|
|
Assert.Equal(valid, TaskSchedule.Validate(schedule).IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void TaskSet_RoundTripsThroughTheJsonContext()
|
|
{
|
|
var set = new TaskSet
|
|
{
|
|
Version = 7,
|
|
Tasks = [new TaskSpec { Id = new TaskId("tsk_1"), Kind = "quic_ping", Params = new Dictionary<string, JsonElement> { ["rate_hz"] = Json("10") } }],
|
|
Peers = [new PeerSpec { NodeId = new Fleet.Core.NodeId("agt_1"), PublicKey = "AAAA" }],
|
|
};
|
|
string text = JsonSerializer.Serialize(set, MoShiroJsonContext.Default.TaskSet);
|
|
TaskSet back = JsonSerializer.Deserialize(text, MoShiroJsonContext.Default.TaskSet)!;
|
|
|
|
Assert.Contains("\"id\":\"tsk_1\"", text);
|
|
Assert.Equal(7, back.Version);
|
|
Assert.Equal("tsk_1", back.Tasks[0].Id.Value);
|
|
Assert.Equal(10, back.Tasks[0].Params["rate_hz"].GetInt64());
|
|
Assert.Equal("agt_1", back.Peers[0].NodeId.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void NodeEnrollMeta_ToleratesUnknownFields_AndKeepsDefaults()
|
|
{
|
|
NodeEnrollMeta meta = JsonSerializer.Deserialize("{\"kinds\":[\"quic_ping\"],\"future\":1}", MoShiroJsonContext.Default.NodeEnrollMeta)!;
|
|
Assert.Equal(0, meta.TcpPort); // 没有端口表示这个节点不监听
|
|
Assert.Equal(["quic_ping"], meta.Kinds);
|
|
}
|
|
|
|
[Fact]
|
|
public void ErrorCodes_AreSnakeCase()
|
|
{
|
|
foreach (System.Reflection.FieldInfo field in typeof(MoShiroErrors).GetFields())
|
|
{
|
|
string value = ((ErrorCode)field.GetValue(null)!).Value;
|
|
Assert.Matches("^[a-z]+(_[a-z]+)*$", value);
|
|
}
|
|
}
|
|
}
|