From d679231fede210fc5373ba9aab2403c588752a3f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 14:25:19 +0200 Subject: [PATCH] test(core): add system parsing and Prometheus tests --- .../Parsing/ParsersTests.cs | 71 +++++++++++++++++++ .../Prometheus/PrometheusParserTests.cs | 56 +++++++++++++++ .../Prometheus/PrometheusProviderTests.cs | 68 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 tests/Vantage.Core.Tests/Parsing/ParsersTests.cs create mode 100644 tests/Vantage.Core.Tests/Prometheus/PrometheusParserTests.cs create mode 100644 tests/Vantage.Core.Tests/Prometheus/PrometheusProviderTests.cs diff --git a/tests/Vantage.Core.Tests/Parsing/ParsersTests.cs b/tests/Vantage.Core.Tests/Parsing/ParsersTests.cs new file mode 100644 index 0000000..ffd15ab --- /dev/null +++ b/tests/Vantage.Core.Tests/Parsing/ParsersTests.cs @@ -0,0 +1,71 @@ +using Vantage.Core.Metrics.Parsing; +using Vantage.Core.Tests.Fixtures; +using Xunit; + +namespace Vantage.Core.Tests.Parsing; + +public class ParsersTests +{ + [Fact] + public void LoadAvgParser_ReadsFirstThreeValues() + { + var load = LoadAvgParser.Parse(MetricsFixtures.Loadavg); + Assert.Equal(0.50, load.Load1, 3); + Assert.Equal(0.40, load.Load5, 3); + Assert.Equal(0.30, load.Load15, 3); + } + + [Fact] + public void UptimeParser_ReadsSeconds() + { + var uptime = UptimeParser.Parse(MetricsFixtures.Uptime); + Assert.Equal(123456.78, uptime.TotalSeconds, 2); + } + + [Fact] + public void MemInfoParser_UsesMemAvailable() + { + var memory = MemInfoParser.Parse(MetricsFixtures.Meminfo); + Assert.Equal(8000000L * 1024, memory.TotalBytes); + Assert.Equal(6000000L * 1024, memory.UsedBytes); + } + + [Fact] + public void MemInfoParser_FallsBackToMemFreeWhenNoAvailable() + { + var memory = MemInfoParser.Parse("MemTotal: 4000 kB\nMemFree: 1000 kB\n"); + Assert.Equal(4000L * 1024, memory.TotalBytes); + Assert.Equal(3000L * 1024, memory.UsedBytes); + } + + [Fact] + public void DfParser_ReadsTotalAndUsedFromDataRow() + { + var disk = DfParser.Parse(MetricsFixtures.Df); + Assert.Equal(100000000000L, disk.TotalBytes); + Assert.Equal(40000000000L, disk.UsedBytes); + } + + [Fact] + public void ProcStatParser_SumsTotalAndIdle() + { + var times = ProcStatParser.Parse(MetricsFixtures.Stat1); + Assert.Equal(1000, times.TotalTime); + Assert.Equal(800, times.IdleTime); + } + + [Fact] + public void CpuUsage_ComputesBusyFractionFromTwoSnapshots() + { + var before = ProcStatParser.Parse(MetricsFixtures.Stat1); + var after = ProcStatParser.Parse(MetricsFixtures.Stat2); + Assert.Equal(80.0, CpuUsage.Compute(before, after), 1); + } + + [Fact] + public void CpuUsage_ReturnsZeroWhenNoDelta() + { + var times = new CpuTimes(800, 1000); + Assert.Equal(0, CpuUsage.Compute(times, times)); + } +} diff --git a/tests/Vantage.Core.Tests/Prometheus/PrometheusParserTests.cs b/tests/Vantage.Core.Tests/Prometheus/PrometheusParserTests.cs new file mode 100644 index 0000000..0d0ede4 --- /dev/null +++ b/tests/Vantage.Core.Tests/Prometheus/PrometheusParserTests.cs @@ -0,0 +1,56 @@ +using Vantage.Core.Prometheus; +using Vantage.Core.Tests.Fixtures; +using Xunit; + +namespace Vantage.Core.Tests.Prometheus; + +public class PrometheusParserTests +{ + [Fact] + public void Parse_SkipsCommentsAndBlankLines() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.Mixed); + Assert.DoesNotContain(samples, s => s.Name.StartsWith("#")); + } + + [Fact] + public void Parse_ReadsLabelsValueAndTimestamp() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.Mixed); + + var post200 = samples.Single(s => + s.Name == "http_requests_total" && s.HasLabel("code", "200")); + Assert.Equal("post", post200.Label("method")); + Assert.Equal(1027, post200.Value); + Assert.Equal(1395066363000, post200.TimestampMs); + } + + [Fact] + public void Parse_MetricWithoutLabels() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.Mixed); + var bare = samples.Single(s => s.Name == "metric_without_labels"); + Assert.Empty(bare.Labels); + Assert.Equal(12.47, bare.Value, 3); + } + + [Fact] + public void Parse_HandlesEscapedLabelValues() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.Mixed); + var msdos = samples.Single(s => s.Name == "msdos_file_access_time_seconds"); + + Assert.Equal("C:\\DIR\\FILE.TXT", msdos.Label("path")); + Assert.Contains("\n", msdos.Label("error")); + Assert.Contains("\"FILE.TXT\"", msdos.Label("error")); + } + + [Fact] + public void Parse_HandlesInfinityAndNaN() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.Mixed); + + Assert.True(double.IsPositiveInfinity(samples.Single(s => s.Name == "something_weird").Value)); + Assert.True(double.IsNaN(samples.Single(s => s.Name == "gauge_nan_value").Value)); + } +} diff --git a/tests/Vantage.Core.Tests/Prometheus/PrometheusProviderTests.cs b/tests/Vantage.Core.Tests/Prometheus/PrometheusProviderTests.cs new file mode 100644 index 0000000..dc364b8 --- /dev/null +++ b/tests/Vantage.Core.Tests/Prometheus/PrometheusProviderTests.cs @@ -0,0 +1,68 @@ +using Vantage.Core.Models; +using Vantage.Core.Prometheus; +using Vantage.Core.Tests.Fakes; +using Vantage.Core.Tests.Fixtures; +using Xunit; + +namespace Vantage.Core.Tests.Prometheus; + +public class PrometheusProviderTests +{ + private static readonly DateTimeOffset Now = new(2026, 8, 26, 12, 0, 0, TimeSpan.Zero); + + [Fact] + public void Mapper_MapsMemoryDiskLoadAndUptime() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.NodeScrape1()); + var metrics = PrometheusNodeMapper.Map(samples, "/", cpuPercent: 0, Now); + + Assert.Equal(8_000_000_000, metrics.MemoryTotalBytes); + Assert.Equal(6_000_000_000, metrics.MemoryUsedBytes); + Assert.Equal(75.0, metrics.MemoryUsedPercent, 1); + Assert.Equal(100_000_000_000, metrics.DiskTotalBytes); + Assert.Equal(40_000_000_000, metrics.DiskUsedBytes); + Assert.Equal(0.5, metrics.Load1, 3); + Assert.Equal(TimeSpan.FromSeconds(86400), metrics.Uptime); + } + + [Fact] + public void Mapper_SelectsFilesystemByMountpoint() + { + var samples = PrometheusTextParser.Parse(PrometheusFixtures.NodeScrape1()); + var boot = PrometheusNodeMapper.Map(samples, "/boot", cpuPercent: 0, Now); + + Assert.Equal(500_000_000, boot.DiskTotalBytes); + Assert.Equal(100_000_000, boot.DiskUsedBytes); + } + + [Fact] + public void CpuPercent_ComputedFromCounterDelta() + { + var first = PrometheusNodeMapper.ExtractCpuCounters(PrometheusTextParser.Parse(PrometheusFixtures.NodeScrape1())); + var second = PrometheusNodeMapper.ExtractCpuCounters(PrometheusTextParser.Parse(PrometheusFixtures.NodeScrape2())); + + Assert.Equal(20.0, PrometheusNodeMapper.CpuPercent(first, second), 1); + } + + [Fact] + public async Task Provider_FirstScrapeZeroCpu_SecondScrapeComputesCpu() + { + var handler = StubHttpMessageHandler.Sequence(PrometheusFixtures.NodeScrape1(), PrometheusFixtures.NodeScrape2()); + var provider = new PrometheusMetricsProvider(new HttpClient(handler), () => Now); + var host = new HostConfig { Id = "h1", Address = "10.0.0.1", MetricsEndpoint = "http://10.0.0.1:9100/metrics" }; + + var first = await provider.GetMetricsAsync(host, CancellationToken.None); + var second = await provider.GetMetricsAsync(host, CancellationToken.None); + + Assert.Equal(0, first.CpuUsagePercent); + Assert.Equal(20.0, second.CpuUsagePercent, 1); + Assert.Equal(75.0, second.MemoryUsedPercent, 1); + } + + [Fact] + public void ResolveMetricsEndpoint_DefaultsToNodeExporterPort() + { + var host = new HostConfig { Address = "10.0.0.5" }; + Assert.Equal("http://10.0.0.5:9100/metrics", host.ResolveMetricsEndpoint()); + } +}