Namespaces, assemblies, projects and directories, the solution, binaries, images, systemd units, config paths, environment variables, protocol method names, the media type and the raw chunk magic all move to MoShiro/moshiro. Existing data and deployments are not compatible and must be recreated. Also fix the architecture tests after the solution layout change and make the update tests independent of their order.
166 lines
8.2 KiB
C#
166 lines
8.2 KiB
C#
using System.Text.Json.Nodes;
|
|
using MoShiro.Agent;
|
|
using MoShiro.Protocol;
|
|
using MoShiro.TestSupport;
|
|
|
|
namespace MoShiro.Backend.Tests;
|
|
|
|
public sealed class HostSamplerTests
|
|
{
|
|
private const string Stat1 = "cpu 100 0 100 700 100 0 0 0 0 0\ncpu0 1 1 1 1 1 0 0 0 0 0\n";
|
|
private const string Stat2 = "cpu 200 0 200 800 100 0 0 0 0 0\n"; // 比上一次:总时间 +300,其中空闲 +100
|
|
|
|
private const string Dev1 = "Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n lo: 999 1 0 0 0 0 0 0 999 1 0 0 0 0 0 0\n eth0: 1000 10 0 0 0 0 0 0 5000 10 0 0 0 0 0 0\ndocker0: 777 1 0 0 0 0 0 0 777 1 0 0 0 0 0 0\n";
|
|
private const string Dev2 = "Inter-| Receive\n face |bytes\n eth0: 3000 10 0 0 0 0 0 0 9000 10 0 0 0 0 0 0\n";
|
|
|
|
[Fact]
|
|
public void Parsers_ReadTheProcFormats_AndSkipLoopbackAndContainerInterfaces()
|
|
{
|
|
Assert.Equal((1000L, 800L), HostSampler.ParseCpu(Stat1));
|
|
Assert.Equal((8L * 1024 * 1024, 2L * 1024 * 1024), HostSampler.ParseMemory("MemTotal: 8192 kB\nMemFree: 100 kB\nMemAvailable: 6144 kB\n"));
|
|
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1));
|
|
Assert.Equal(3600L, HostSampler.ParseUptime("3600.55 7000.1\n"));
|
|
Assert.Null(HostSampler.ParseNetwork("nothing useful"));
|
|
Assert.Equal((1000L, 5000L), HostSampler.ParseNetwork(Dev1 + "podman0: 8888 1 0 0 0 0 0 0 9999 1 0 0 0 0 0 0\n"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sample_DerivesCpuPercentAndNetworkSpeedFromTheDifferenceBetweenTwoReads()
|
|
{
|
|
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");
|
|
HostSnapshot first = sampler.Sample();
|
|
Assert.Null(first.CpuPercent); // 第一次没有基线
|
|
Assert.Null(first.NetRxBps);
|
|
Assert.Equal(250, first.DiskUsed);
|
|
Assert.Equal(100, first.UptimeSeconds);
|
|
|
|
time.Advance(TimeSpan.FromSeconds(2));
|
|
stat = Stat2;
|
|
dev = Dev2;
|
|
HostSnapshot second = sampler.Sample();
|
|
Assert.Equal(66.7, second.CpuPercent); // 总时间 +300、空闲 +100:忙碌 200/300
|
|
Assert.Equal(1000, second.NetRxBps); // (3000 - 1000) / 2 s
|
|
Assert.Equal(2000, second.NetTxBps);
|
|
Assert.Equal(3000, second.NetRxTotal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sample_UsesMountedHostProcAndSys_InsteadOfContainerPaths()
|
|
{
|
|
var reads = new List<string>();
|
|
var sampler = new HostSampler(TimeProvider.System, path =>
|
|
{
|
|
reads.Add(path);
|
|
return path switch
|
|
{
|
|
"/host/proc/stat" => Stat1,
|
|
"/host/proc/meminfo" => "MemTotal: 8192 kB\nMemAvailable: 6144 kB\n",
|
|
"/host/proc/net/dev" => Dev1,
|
|
"/host/proc/uptime" => "3600.0 7000.0",
|
|
"/host/sys/class/net/eth0/speed" => "10000\n",
|
|
_ => throw new InvalidOperationException("unexpected container path: " + path),
|
|
};
|
|
}, () => (1000, 250), procRoot: "/host/proc", sysRoot: "/host/sys");
|
|
|
|
HostSnapshot snapshot = sampler.Sample();
|
|
Assert.Equal(8L * 1024 * 1024, snapshot.MemTotal);
|
|
Assert.Equal(1000, snapshot.NetRxTotal);
|
|
Assert.Equal(10000, snapshot.NetLinkMbps);
|
|
Assert.Equal(3600, snapshot.UptimeSeconds);
|
|
Assert.DoesNotContain(reads, path => path.StartsWith("/proc/", StringComparison.Ordinal));
|
|
Assert.DoesNotContain(reads, path => path.Contains("docker0", StringComparison.Ordinal) || path.Contains("/lo/", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("-1")]
|
|
[InlineData("0")]
|
|
[InlineData("not supported")]
|
|
public void Sample_DoesNotInventLinkSpeedOrContainerDisk_WhenHostDataIsUnavailable(string? speed)
|
|
{
|
|
var sampler = new HostSampler(TimeProvider.System,
|
|
path => path.EndsWith("net/dev", StringComparison.Ordinal) ? Dev1 : path.EndsWith("/speed", StringComparison.Ordinal) ? speed : null,
|
|
procRoot: "/host/proc", sysRoot: "/host/sys", diskRoot: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
|
|
|
|
HostSnapshot snapshot = sampler.Sample();
|
|
Assert.Null(snapshot.NetLinkMbps);
|
|
Assert.Null(snapshot.DiskTotal);
|
|
Assert.Null(snapshot.DiskUsed);
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
Assert.Null(snapshot.UptimeSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Collection("backend")]
|
|
public sealed class HostStatusTests
|
|
{
|
|
private readonly BackendFixture _backend;
|
|
|
|
public HostStatusTests(BackendFixture backend)
|
|
{
|
|
_backend = backend;
|
|
}
|
|
|
|
private static HostSnapshot Snapshot(double cpu) => new()
|
|
{
|
|
CpuPercent = cpu, MemUsed = 2L << 30, MemTotal = 8L << 30, DiskUsed = 10L << 30, DiskTotal = 40L << 30,
|
|
NetRxTotal = 600L << 30, NetTxTotal = 180L << 30, NetRxBps = 2500, NetTxBps = 4800, UptimeSeconds = 3_000_000,
|
|
NetLinkMbps = 1000,
|
|
};
|
|
|
|
[Fact]
|
|
public async Task AgentHostSnapshots_AreVisibleToTheDisplayInRealTime_StoredAsHistory_AndNeverCarryModelsOrAddresses()
|
|
{
|
|
await _backend.SiteMustBePublicAsync();
|
|
await using AgentSimulator agent = await _backend.ConnectAgentAsync();
|
|
string id = agent.NodeId.Value;
|
|
|
|
await using SseClient sse = await SseClient.ConnectAsync(_backend.Anonymous, "/api/public/live?topics=hosts");
|
|
await agent.SendHostAsync(Snapshot(18.5));
|
|
SseEvent live = await sse.WaitForAsync(e => e.Name == "host" && e.Data!["agent_id"]!.GetValue<string>() == id);
|
|
Assert.Equal(18.5, live.Data!["host"]!["cpu_percent"]!.GetValue<double>());
|
|
Assert.True(live.Data["online"]!.GetValue<bool>());
|
|
|
|
// REST:最新值,没有过期
|
|
JsonNode list = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
|
JsonNode item = list["items"]!.AsArray().Single(i => i!["agent_id"]!.GetValue<string>() == id)!;
|
|
Assert.False(item["stale"]!.GetValue<bool>());
|
|
Assert.Equal(40L << 30, item["host"]!["disk_total"]!.GetValue<long>());
|
|
Assert.Equal(1000, item["host"]!["net_link_mbps"]!.GetValue<long>());
|
|
|
|
// 展示页能拿到的只有汇总数字:整份响应里没有型号、主机名、地址之类的字段
|
|
string text = list.ToJsonString();
|
|
foreach (string forbidden in new[] { "model", "hostname", "\"ip\"", "address", "remote", "mac", "iface", "public_host" })
|
|
{
|
|
Assert.DoesNotContain(forbidden, text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
// 线路详情也不泄漏 Probe 的地址和版本
|
|
string line = (await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/lines/{_backend.Line(id)}")).ToJsonString();
|
|
Assert.DoesNotContain("remote", line, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("probe_version", line, StringComparison.OrdinalIgnoreCase);
|
|
|
|
// 历史:每分钟一个样本
|
|
JsonNode history = null!;
|
|
await BackendFixture.EventuallyAsync(async () =>
|
|
{
|
|
history = await _backend.GetJsonAsync(_backend.Anonymous, $"/api/public/agents/{id}/host?range=1h");
|
|
return history["values"]!["cpu"]!.AsArray().Any(v => v is not null);
|
|
});
|
|
Assert.Equal(25.0, history["values"]!["mem"]!.AsArray().Last(v => v is not null)!.GetValue<double>());
|
|
Assert.Equal(2500, history["values"]!["rx_bps"]!.AsArray().Last(v => v is not null)!.GetValue<double>());
|
|
|
|
// 隐藏的 Agent 对匿名访客不可见
|
|
await _backend.Admin.PatchAsync($"/api/admin/agents/{id}", BackendFixture.Json(new JsonObject { ["public_visible"] = false }));
|
|
JsonNode hidden = await _backend.GetJsonAsync(_backend.Anonymous, "/api/public/hosts");
|
|
Assert.DoesNotContain(hidden["items"]!.AsArray(), i => i!["agent_id"]!.GetValue<string>() == id);
|
|
using HttpResponseMessage denied = await _backend.Anonymous.GetAsync($"/api/public/agents/{id}/host");
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, denied.StatusCode);
|
|
}
|
|
}
|