Files

109 lines
6.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json.Nodes;
using Fleet.Core;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
using MoShiro.TestSupport;
namespace MoShiro.Backend.Tests;
public sealed class AdaptiveAuditTests : IClassFixture<BackendFixture>
{
private static readonly long s_t0 = new DateTimeOffset(2026, 10, 22, 10, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds();
private const long Minute = 60_000;
private const long Hour = 3_600_000;
private readonly BackendFixture _backend;
public AdaptiveAuditTests(BackendFixture backend)
{
_backend = backend;
}
private static RateChange Change(long atMs, string kind, string from, string to, double hz, string reason = "quiet", double lossPct = 0) =>
new() { AtMs = atMs, Kind = kind, From = from, To = to, Hz = hz, Reason = reason, LossPct = lossPct, Sent = 1000, Lost = (int)(lossPct * 10) };
/// <summary>状态变化还原成每个时间桶里各状态的秒数:跨桶的一段要切开,区间开头的状态取第一次变化的「从」,不同方法各算各的。</summary>
[Fact]
public void Summarize_SplitsStateTimeAcrossBuckets_AndWeightsTheAverageRateByTime()
{
RateChange[] changes =
[
Change(s_t0, "quic_ping", "start", "watch", 10, "start"), // 10:00 起步:watch 10 Hz
Change(s_t0 + (30 * Minute), "quic_ping", "watch", "quiet", 2), // 10:30 起 quiet 2 Hz
Change(s_t0 + (90 * Minute), "quic_ping", "quiet", "active", 10, "sustained loss", 0.4), // 11:30 起 active 10 Hz
Change(s_t0 + (30 * Minute), "tcp_ping", "quiet", "watch", 20, "rtt above baseline"), // 这条方法 10:30 才有第一条记录
];
AdaptiveResult result = AdaptiveAudit.Summarize(changes, s_t0, s_t0 + (2 * Hour), Hour);
AdaptiveKind quic = Assert.Single(result.Kinds, k => k.Kind == "quic_ping");
Assert.Equal(2, quic.Buckets.Count);
AdaptiveBucket first = quic.Buckets[0];
Assert.Equal((1800, 1800, 0), ((int)first.WatchSeconds, (int)first.QuietSeconds, (int)first.ActiveSeconds)); // 10:00–10:30 watch,10:30–11:00 quiet
Assert.Equal(6, first.AverageHz); // (10 Hz × 30 min + 2 Hz × 30 min) / 60 min
Assert.Equal(2, first.Changes);
AdaptiveBucket second = quic.Buckets[1];
Assert.Equal((1800, 1800), ((int)second.QuietSeconds, (int)second.ActiveSeconds)); // 11:00–11:30 quiet,11:30–12:00 active
Assert.Equal(6, second.AverageHz); // (2 Hz × 30 min + 10 Hz × 30 min) / 60 min
Assert.Equal((1, 0.4), (second.Changes, second.MaxLossPct));
AdaptiveKind tcp = Assert.Single(result.Kinds, k => k.Kind == "tcp_ping");
Assert.Equal((0, 1800), ((int)tcp.Buckets[0].QuietSeconds, (int)tcp.Buckets[0].WatchSeconds)); // 10:00–10:30 没有记录,不算任何状态
Assert.Equal(3600, (int)tcp.Buckets[1].WatchSeconds);
Assert.Equal(4, result.Changes.Count);
Assert.Equal("quic_ping", result.Changes[0].Kind); // 最新的在前
}
/// <summary>变化在区间开头之前发生:那之后的状态延续到区间里。</summary>
[Fact]
public void Summarize_CarriesTheStateInFromBeforeTheRange()
{
RateChange[] changes = [Change(s_t0 - (2 * Hour), "quic_ping", "quiet", "active", 10, "sustained loss", 1)];
AdaptiveResult result = Summarize(changes);
AdaptiveBucket bucket = Assert.Single(Assert.Single(result.Kinds).Buckets);
Assert.Equal(3600, (int)bucket.ActiveSeconds);
Assert.Equal((10, 0), (bucket.AverageHz, bucket.Changes));
Assert.Empty(result.Changes); // 区间里没有变化
AdaptiveResult Summarize(RateChange[] c) => AdaptiveAudit.Summarize(c, s_t0, s_t0 + Hour, Hour);
}
/// <summary>端到端:拨号节点随心跳上报的状态变化存成事件,几天后还能通过接口按时段查到。</summary>
[Fact]
public async Task RateChangesReportedInHeartbeats_AreStored_AndServedByTheAdaptiveEndpoint()
{
await using AgentSimulator first = await _backend.ConnectAgentAsync();
await using AgentSimulator second = await _backend.ConnectAgentAsync();
(AgentSimulator dialer, AgentSimulator listener) = string.CompareOrdinal(first.NodeId.Value, second.NodeId.Value) < 0 ? (first, second) : (second, first); // 小 id 拨大 id
long now = TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds();
RateChange Of(long atMs, string from, string to, double hz, string reason, double loss)
{
RateChange change = Change(atMs, "quic_ping", from, to, hz, reason, loss);
change.Listener = listener.NodeId.Value;
change.IpVersion = "ipv4";
return change;
}
await dialer.SendRateChangesAsync(Of(now - (20 * Minute), "watch", "quiet", 2, "quiet", 0), Of(now - (10 * Minute), "quiet", "active", 10, "sustained loss", 0.5));
string lineId = listener.NodeId.Value + "~" + dialer.NodeId.Value;
JsonNode? audit = null;
await BackendFixture.EventuallyAsync(async () =>
{
audit = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/lines/{lineId}/adaptive?range=1h&step=10m");
return audit["changes"]!.AsArray().Count == 2;
});
JsonNode quic = Assert.Single(audit!["kinds"]!.AsArray())!;
Assert.Equal("quic_ping", quic["kind"]!.GetValue<string>());
double active = quic["buckets"]!.AsArray().Sum(b => b!["active_seconds"]!.GetValue<double>());
Assert.InRange(active, 500, 700); // 最近 10 分钟 active
JsonNode newest = audit!["changes"]![0]!;
Assert.Equal(("quiet", "active", "sustained loss"), (newest["from"]!.GetValue<string>(), newest["to"]!.GetValue<string>(), newest["reason"]!.GetValue<string>()));
}
}