shared: add independent IPv4 and IPv6 collection [CI]
This commit is contained in:
1 parent
c974a7dddb
commit
0eda0600cc
60 files changed
+699
-137
No files matched your search
@@ -30,7 +30,7 @@ public sealed class HostSamplerTests
|
||||
var time = new Microsoft.Extensions.Time.Testing.FakeTimeProvider();
|
||||
string stat = Stat1;
|
||||
string dev = Dev1;
|
||||
var sampler = new HostSampler(time, path => path switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250), procRoot: "/proc", sysRoot: "/sys");
|
||||
var sampler = new HostSampler(time, path => path.Replace('\\', '/') switch { "/proc/stat" => stat, "/proc/net/dev" => dev, "/proc/uptime" => "100.0 1.0", _ => null }, () => (1000, 250), procRoot: "/proc", sysRoot: "/sys");
|
||||
HostSnapshot first = sampler.Sample();
|
||||
Assert.Null(first.CpuPercent); // 第一次没有基线
|
||||
Assert.Null(first.NetRxBps);
|
||||
@@ -53,6 +53,7 @@ public sealed class HostSamplerTests
|
||||
var reads = new List<string>();
|
||||
var sampler = new HostSampler(TimeProvider.System, path =>
|
||||
{
|
||||
path = path.Replace('\\', '/'); // 测试里的 /proc 格式统一用斜杠,兼容 Windows 的 Path.Combine
|
||||
reads.Add(path);
|
||||
return path switch
|
||||
{
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MoShiro.Backend.Application;
|
||||
using MoShiro.Protocol;
|
||||
using MoShiro.TestSupport;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>真实双栈回环:同一身份的两种会话、原始块、查询与测试结果必须各自独立。</summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class Ipv6Tests
|
||||
{
|
||||
private readonly BackendFixture _backend;
|
||||
|
||||
public Ipv6Tests(BackendFixture backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Queries_DifferentFamilyLoss_RemainsIndependentInHotPersistedAndCompareViews()
|
||||
{
|
||||
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
||||
long start = ((TimeProvider.System.GetUtcNow().ToUnixTimeMilliseconds() / 60_000) - 4) * 60_000;
|
||||
for (int k = 0; k < 6; k++)
|
||||
{
|
||||
foreach (string version in new[] { IpVersions.V4, IpVersions.V6 })
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Echo(agent.NodeId, "udp_echo", start + k * 10_000, 100, 100,
|
||||
i => new EchoPacket(false, version == IpVersions.V6 && i % 2 == 0, 20_000, 20_000));
|
||||
chunk.Header.ProbeId = new ProbeId(_backend.DefaultProbeId);
|
||||
chunk.Header.IpVersion = version;
|
||||
Assert.True(await agent.SendChunkAsync(chunk));
|
||||
}
|
||||
}
|
||||
|
||||
string range = "from=" + DateTimeOffset.FromUnixTimeMilliseconds(start).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
+ "&to=" + DateTimeOffset.FromUnixTimeMilliseconds(start + 60_000).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
||||
string v4 = _backend.Line(agent.NodeId);
|
||||
string v6 = v4 + "~ipv6";
|
||||
foreach (string step in new[] { "1s", "10s", "1m" })
|
||||
{
|
||||
foreach ((string line, double loss) in new[] { (v4, 0.0), (v6, 50.0) })
|
||||
{
|
||||
JsonNode series = null!;
|
||||
await BackendFixture.EventuallyAsync(async () =>
|
||||
{
|
||||
series = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/lines/{line}/series?{range}&step={step}&preset=udp");
|
||||
return series["summary"]!["samples"]!["udp_echo"]?.GetValue<long>() == 600;
|
||||
});
|
||||
Assert.Equal(loss, series["summary"]!["metrics"]!["loss.total"]!.GetValue<double>(), 6);
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode compare = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/compare?lines={v4},{v6}&metric=loss.total&step=1m&{range}");
|
||||
Assert.Equal(0, compare["summary"]![v4]!.GetValue<double>());
|
||||
Assert.Equal(50, compare["summary"]![v6]!.GetValue<double>());
|
||||
|
||||
await using SseClient live = await SseClient.ConnectAsync(_backend.Admin, $"/api/public/live?topics=series:{v6}:10s:udp");
|
||||
SseEvent snapshot = await live.WaitForAsync(e => e.Name == "series" && e.Data?["line_id"]?.GetValue<string>() == v6);
|
||||
Assert.Equal("ipv6", snapshot.Data!["ip_version"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ipv6Configuration_RejectsWrongFamily_AndDoesNotArrangeIpv6ForALegacyAgent()
|
||||
{
|
||||
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
||||
using HttpResponseMessage invalid = await _backend.Admin.PatchAsync("/api/admin/agents/" + agent.NodeId.Value,
|
||||
BackendFixture.Json(new JsonObject { ["ipv6_host"] = "127.0.0.1" }));
|
||||
Assert.Equal(HttpStatusCode.BadRequest, invalid.StatusCode);
|
||||
Assert.Contains("ipv6_host", await invalid.Content.ReadAsStringAsync(), StringComparison.Ordinal);
|
||||
using HttpResponseMessage valid = await _backend.Admin.PatchAsync("/api/admin/agents/" + agent.NodeId.Value,
|
||||
BackendFixture.Json(new JsonObject { ["ipv6_host"] = "::1" }));
|
||||
Assert.True(valid.IsSuccessStatusCode);
|
||||
ProbeAgentList endpoints = await _backend.Services.GetRequiredService<AgentService>().ProbeAgentsAsync(_backend.DefaultProbeId, default);
|
||||
Assert.Null(Assert.Single(endpoints.Agents, a => a.AgentId == agent.NodeId).Ipv6Host);
|
||||
JsonNode identity = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.NodeId.Value);
|
||||
Assert.Equal("::1", identity["ipv6_host"]!.GetValue<string>());
|
||||
Assert.False(identity["ipv6_supported"]!.GetValue<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DualStack_CollectsBothFamilies_DisablingIpv6KeepsIpv4Running()
|
||||
{
|
||||
Assert.SkipUnless(Socket.OSSupportsIPv6, "IPv6 sockets are unavailable on this host");
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend, c => c.Listen.Bind = "::");
|
||||
await agent.OverrideLineTasksAsync(rateHz: 30);
|
||||
using HttpResponseMessage configured = await _backend.Admin.PatchAsync("/api/admin/agents/" + agent.Id,
|
||||
BackendFixture.Json(new JsonObject { ["public_host"] = "127.0.0.1", ["ipv6_host"] = "::1" }));
|
||||
Assert.True(configured.IsSuccessStatusCode, await configured.Content.ReadAsStringAsync());
|
||||
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
|
||||
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.IsConnected(agent.Id) && probe.App.IsConnected(agent.Id, IpVersions.V6)));
|
||||
await BackendFixture.EventuallyAsync(async () =>
|
||||
{
|
||||
JsonNode raw = await _backend.GetJsonAsync(_backend.Admin, "/api/admin/raw/chunks?agent_id=" + agent.Id);
|
||||
return new[] { "line:" + probe.ProbeId, "line:" + probe.ProbeId + ":ipv6" }.All(series =>
|
||||
new[] { "udp_echo", "tcp_echo" }.All(kind => raw["items"]!.AsArray().Any(c => c!["series"]!.GetValue<string>() == series && c["kind"]!.GetValue<string>() == kind)));
|
||||
}, TimeSpan.FromSeconds(45));
|
||||
|
||||
string v4 = agent.Id + "~" + probe.ProbeId;
|
||||
string v6 = v4 + "~ipv6";
|
||||
foreach (string step in new[] { "1s", "10s" })
|
||||
{
|
||||
foreach ((string line, string family) in new[] { (v4, IpVersions.V4), (v6, IpVersions.V6) })
|
||||
{
|
||||
JsonNode series = await _backend.GetJsonAsync(_backend.Admin, $"/api/public/lines/{line}/series?range=1m&step={step}&preset=udp,tcp");
|
||||
Assert.Equal(family, series["ip_version"]!.GetValue<string>());
|
||||
Assert.True(series["summary"]!["samples"]!["udp_echo"]!.GetValue<long>() > 0, series.ToJsonString());
|
||||
Assert.True(series["summary"]!["samples"]!["tcp_echo"]!.GetValue<long>() > 0, series.ToJsonString());
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode overview = await _backend.GetJsonAsync(_backend.Admin, "/api/public/overview?range=1m&ip_version=ipv6");
|
||||
Assert.Contains(overview["lines"]!.AsArray(), l => l!["id"]!.GetValue<string>() == v6);
|
||||
Assert.All(overview["lines"]!.AsArray(), l => Assert.Equal("ipv6", l!["ip_version"]!.GetValue<string>()));
|
||||
|
||||
var ipv4Connection = probe.App.Connection(agent.Id);
|
||||
long previous = agent.App.Counters.Fragments;
|
||||
using HttpResponseMessage disabled = await _backend.Admin.PatchAsync("/api/admin/agents/" + agent.Id, BackendFixture.Json(new JsonObject { ["ipv6_host"] = "" }));
|
||||
Assert.True(disabled.IsSuccessStatusCode);
|
||||
await BackendFixture.EventuallyAsync(() => Task.FromResult(probe.App.Connection(agent.Id, IpVersions.V6) is null));
|
||||
Assert.Same(ipv4Connection, probe.App.Connection(agent.Id));
|
||||
Assert.True(probe.App.IsConnected(agent.Id));
|
||||
await BackendFixture.EventuallyAsync(() => Task.FromResult(agent.App.Counters.Fragments > previous), TimeSpan.FromSeconds(20));
|
||||
Assert.Equal(0, agent.App.Counters.FragmentsDiscarded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("hy2_ladder")]
|
||||
[InlineData("reality_session")]
|
||||
public async Task Ipv6Test_UsesIpv6Session_AndDoesNotAppearInIpv4History(string plan)
|
||||
{
|
||||
Assert.SkipUnless(Socket.OSSupportsIPv6, "IPv6 sockets are unavailable on this host");
|
||||
await using AgentHarness agent = await AgentHarness.StartAsync(_backend, c => c.Listen.Bind = "::");
|
||||
await agent.OverrideLineTasksAsync();
|
||||
using HttpResponseMessage configured = await _backend.Admin.PatchAsync("/api/admin/agents/" + agent.Id,
|
||||
BackendFixture.Json(new JsonObject { ["ipv6_host"] = "::1" }));
|
||||
Assert.True(configured.IsSuccessStatusCode);
|
||||
await using ProbeHarness probe = await ProbeHarness.StartAsync(_backend);
|
||||
string v4 = agent.Id + "~" + probe.ProbeId;
|
||||
string v6 = v4 + "~ipv6";
|
||||
await BackendFixture.EventuallyAsync(async () =>
|
||||
(await _backend.GetJsonAsync(_backend.Admin, "/api/admin/agents/" + agent.Id))["probes"]!.AsArray()
|
||||
.Any(p => p!["probe_id"]!.GetValue<string>() == probe.ProbeId && p["ip_version"]!.GetValue<string>() == "ipv6"));
|
||||
var parameters = plan == "hy2_ladder" ? new JsonObject { ["mbps"] = 1, ["duration_s"] = 1 } : new JsonObject { ["duration_s"] = 1 };
|
||||
using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{v6}/tests", BackendFixture.Json(new JsonObject { ["plan"] = plan, ["params"] = parameters }));
|
||||
Assert.Equal(HttpStatusCode.Accepted, accepted.StatusCode);
|
||||
string id = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue<string>();
|
||||
JsonNode result = null!;
|
||||
await BackendFixture.EventuallyAsync(async () =>
|
||||
{
|
||||
result = await _backend.GetJsonAsync(_backend.Admin, "/api/public/tests/" + id);
|
||||
return result["status"]!.GetValue<string>() is "failed" or "aborted" || result["result"] is not null;
|
||||
}, TimeSpan.FromSeconds(100));
|
||||
Assert.Equal("succeeded", result["status"]!.GetValue<string>());
|
||||
Assert.Equal("ipv6", result["ip_version"]!.GetValue<string>());
|
||||
Assert.NotNull(result["result"]);
|
||||
JsonNode history = await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/lines/{v4}/tests");
|
||||
Assert.Empty(history["items"]!.AsArray());
|
||||
JsonNode v6History = await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/lines/{v6}/tests");
|
||||
Assert.Contains(v6History["items"]!.AsArray(), t => t!["id"]!.GetValue<string>() == id);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user