using SQLInsight.Core.Servers; namespace SQLInsight.Core.Tests; public class DiskForecastTests { private static readonly DateTimeOffset Now = new(2026, 10, 2, 12, 0, 0, TimeSpan.Zero); /// Hourly samples for days back, free space from a function of the age in days. private static List History(int days, Func free, double size = 100) => Enumerable.Range(0, (days * 24) + 1).Select(h => Now.AddHours(-h)).Select(at => new DiskHistoryPoint(at, size, free((Now - at).TotalDays))).Reverse().ToList(); [Fact] public void Steady_growth_gives_days_until_full() { // 1 GB per day used, 20 GB free now: full in 20 days. var forecast = DiskForecaster.Forecast(@"D:\", History(14, age => 20 + age), Now); Assert.NotNull(forecast.DaysUntilFull); Assert.Equal(20, forecast.DaysUntilFull!.Value, 1); Assert.Equal(1, forecast.GrowthGbPerDay, 3); Assert.Equal(1, forecast.Fit, 3); Assert.Equal("fuld om ca. 20 dage", forecast.Describe()); Assert.Equal(Now.AddDays(20).Date, forecast.FullAt(Now)!.Value.Date); } [Fact] public void Noise_around_a_trend_still_forecasts() { var random = new Random(7); var forecast = DiskForecaster.Forecast(@"D:\", History(14, age => 30 + (2 * age) + (random.NextDouble() - 0.5)), Now); Assert.InRange(forecast.DaysUntilFull!.Value, 14, 16); } [Fact] public void A_flat_or_shrinking_volume_has_no_forecast() { Assert.Null(DiskForecaster.Forecast(@"C:\", History(14, _ => 40), Now).DaysUntilFull); var emptying = DiskForecaster.Forecast(@"C:\", History(14, age => 40 - age), Now); Assert.Null(emptying.DaysUntilFull); Assert.Equal("fyldes ikke op", emptying.Note); } [Fact] public void A_sawtooth_of_backups_written_and_deleted_is_not_a_trend() { // Fills 10 GB every night and is cleaned up every morning. var forecast = DiskForecaster.Forecast(@"E:\", History(14, age => 50 - ((age % 1) * 10)), Now); Assert.Null(forecast.DaysUntilFull); } [Fact] public void Too_little_history_says_so() { var forecast = DiskForecaster.Forecast(@"D:\", History(1, age => 20 + (5 * age)), Now); Assert.Null(forecast.DaysUntilFull); Assert.Contains("for lidt historik", forecast.Note, StringComparison.Ordinal); } [Fact] public void An_extended_volume_starts_a_new_trend() { // Was 100 GB and filling fast until 5 days ago; extended to 200 GB and now flat. var history = History(14, age => age > 5 ? 5 + age : 100).Select(p => (Now - p.At).TotalDays > 5 ? p : p with { SizeGb = 200 }).ToList(); var forecast = DiskForecaster.Forecast(@"D:\", history, Now); Assert.Null(forecast.DaysUntilFull); Assert.InRange(forecast.HistoryDays, 4.9, 5.1); } [Fact] public void A_full_volume_is_full_now_and_far_futures_are_not_forecasts() { Assert.Equal(0, DiskForecaster.Forecast(@"D:\", History(14, age => Math.Max(0, age - 1)), Now).DaysUntilFull); var slow = DiskForecaster.Forecast(@"D:\", History(14, age => 500 + (0.5 * age)), Now, new DiskForecastSettings()); Assert.Null(slow.DaysUntilFull); Assert.Equal("fuld om mere end et år", slow.Note); } [Fact] public void Samples_older_than_the_history_window_are_ignored() { // Fast growth 20–40 days ago, flat for the last 14. var history = Enumerable.Range(0, 40 * 24).Select(h => Now.AddHours(-h)) .Select(at => new DiskHistoryPoint(at, 100, (Now - at).TotalDays > 20 ? (Now - at).TotalDays : 30)).Reverse().ToList(); Assert.Null(DiskForecaster.Forecast(@"D:\", history, Now).DaysUntilFull); } } public class ServerPerformanceTests { private static readonly DateTimeOffset T0 = new(2026, 10, 2, 12, 0, 0, TimeSpan.Zero); [Fact] public void Cpu_is_one_minus_the_idle_share_of_the_interval() { var first = new ServerPerformanceRaw { CapturedAt = T0, ProcessorIdle100Ns = 1_000_000_000, ProcessorTimestamp100Ns = 5_000_000_000 }; // One minute = 600,000,000 × 100 ns; idle 150,000,000 of it → 75 % busy. var second = first with { CapturedAt = T0.AddMinutes(1), ProcessorIdle100Ns = 1_150_000_000, ProcessorTimestamp100Ns = 5_600_000_000 }; Assert.Equal(75, ServerPerformanceMath.Cpu(first, second)!.Value, 6); Assert.Null(ServerPerformanceMath.Cpu(null, second)); Assert.Null(ServerPerformanceMath.Cpu(second, first)); // counters went backwards: rebooted } [Fact] public void Pages_per_second_uses_the_performance_frequency() { var first = new ServerPerformanceRaw { CapturedAt = T0, Pages = 1000, MemoryPerfTime = 10_000_000, MemoryPerfFrequency = 10_000_000 }; var second = first with { CapturedAt = T0.AddMinutes(1), Pages = 61_000, MemoryPerfTime = 610_000_000 }; Assert.Equal(1000, ServerPerformanceMath.PagesPerSecond(first, second)!.Value, 6); } [Fact] public void Sample_has_available_percent_and_commit() { var raw = new ServerPerformanceRaw { CapturedAt = T0, AvailableMb = 1024, CommittedBytes = 9, CommitLimit = 10 }; var sample = ServerPerformanceMath.Sample(null, raw, new ServerOsInfo { TotalMemoryMb = 8192 }); Assert.Equal(12.5, sample.AvailablePercent!.Value, 6); Assert.Equal(90, sample.CommitPercent!.Value, 6); Assert.Null(sample.CpuPercent); } [Fact] public void Sustained_needs_most_of_the_period_covered() { var samples = Enumerable.Range(0, 15).Select(m => new ServerPerformanceSample(T0.AddMinutes(-m), 95, null, null, null, null)).Reverse().ToList(); Assert.Equal(95, ServerPerformanceMath.SustainedAverage(samples, s => s.CpuPercent, TimeSpan.FromMinutes(15), T0, TimeSpan.FromMinutes(1))); Assert.Null(ServerPerformanceMath.SustainedAverage(samples.TakeLast(5).ToList(), s => s.CpuPercent, TimeSpan.FromMinutes(15), T0, TimeSpan.FromMinutes(1))); } [Fact] public void The_window_keeps_an_hour() { IReadOnlyList window = []; for (var m = 0; m < 90; m++) { window = ServerPerformanceMath.Append(window, new ServerPerformanceSample(T0.AddMinutes(m), 10, null, null, null, null)); } Assert.Equal(60, window.Count); Assert.Equal(T0.AddMinutes(89), window[^1].At); } } public class ServerEventTests { private static readonly DateTimeOffset Now = new(2026, 10, 2, 12, 0, 0, TimeSpan.Zero); private static ServerEventEntry Entry(string source, long id, int level = 2, string log = "System", int minutesAgo = 1, string? message = "tekst") => new(log, source, id, level, Now.AddMinutes(-minutesAgo), message); [Theory] [InlineData("Microsoft-Windows-Kernel-Power", 41, 1, ServerEventCategory.UnexpectedShutdown)] [InlineData("EventLog", 6008, 2, ServerEventCategory.UnexpectedShutdown)] [InlineData("disk", 153, 3, ServerEventCategory.Disk)] [InlineData("storvsc", 129, 3, ServerEventCategory.Disk)] [InlineData("Ntfs", 55, 2, ServerEventCategory.Disk)] [InlineData("Srv", 2019, 2, ServerEventCategory.Resources)] [InlineData("Service Control Manager", 7031, 2, ServerEventCategory.ServiceFailure)] [InlineData("Application Error", 1000, 2, ServerEventCategory.ApplicationCrash)] [InlineData("Microsoft Dynamics AX Object Server 6.0$01", 110, 2, ServerEventCategory.DynamicsAx)] [InlineData("Some-Driver", 5, 1, ServerEventCategory.Critical)] [InlineData("Some-App", 5, 2, ServerEventCategory.Error)] public void Categorize(string source, long id, int level, ServerEventCategory expected) => Assert.Equal(expected, ServerEventAnalysis.Categorize(Entry(source, id, level))); [Fact] public void Repeated_events_are_one_group_with_the_newest_sample() { var (groups, ignored) = ServerEventAnalysis.Group( [ Entry("Service Control Manager", 7031, minutesAgo: 30, message: "AOS stoppede (1)"), Entry("Service Control Manager", 7031, minutesAgo: 5, message: "AOS stoppede (2)"), Entry("service control manager", 7031, minutesAgo: 10, message: null), Entry("Some-App", 9, log: "Application"), Entry("Some-App", 9, level: 3, log: "Application"), // a warning: not collected ], ServerEventAnalysis.DefaultIgnore); Assert.Equal(0, ignored); Assert.Equal(2, groups.Count); var scm = groups[0]; Assert.Equal(ServerEventCategory.ServiceFailure, scm.Category); Assert.Equal(3, scm.Count); Assert.Equal("AOS stoppede (2)", scm.Sample); Assert.Equal(Now.AddMinutes(-30), scm.FirstAt); Assert.Equal(Now.AddMinutes(-5), scm.LastAt); } [Fact] public void Known_noise_and_sql_server_sources_are_left_out() { var (groups, ignored) = ServerEventAnalysis.Group( [ Entry("Microsoft-Windows-DistributedCOM", 10016), Entry("Schannel", 36887), Entry("MSSQL$PROD", 17053, log: "Application"), Entry("MSSQLSERVER", 823, log: "Application"), Entry("Schannel", 36871), ], ServerEventAnalysis.DefaultIgnore); Assert.Equal(4, ignored); Assert.Equal(36871, Assert.Single(groups).EventId); } [Theory] [InlineData("Some-App:9", true)] [InlineData("Some-App", true)] [InlineData("Some*", true)] [InlineData("Some*:10", false)] [InlineData("Some-App:*", true)] [InlineData("Other:9", false)] public void Ignore_patterns(string pattern, bool ignored) => Assert.Equal(ignored, ServerEventAnalysis.IsIgnored(Entry("Some-App", 9), [pattern])); [Fact] public void Recent_merges_groups_of_the_last_day_and_drops_older() { var (earlier, _) = ServerEventAnalysis.Group([Entry("disk", 153, minutesAgo: 60 * 20), Entry("Some-App", 9, minutesAgo: 60 * 30)], []); var (current, _) = ServerEventAnalysis.Group([Entry("disk", 153, minutesAgo: 2)], []); var recent = ServerEventAnalysis.Recent(earlier, current, Now); var disk = Assert.Single(recent); Assert.Equal(2, disk.Count); Assert.Equal(Now.AddHours(-20), disk.FirstAt); Assert.Equal(Now.AddMinutes(-2), disk.LastAt); } [Fact] public void Samples_are_cut() { var (groups, _) = ServerEventAnalysis.Group([Entry("Some-App", 9, message: new string('x', 2000))], []); Assert.Equal(500, groups[0].Sample!.Length); } } public class ServerConfigurationTests { [Theory] [InlineData("aos01", null)] [InlineData("aos01.firma.local", null)] [InlineData("10.0.0.5", null)] [InlineData("fe80::1", null)] [InlineData(@"sql01\PROD", "uden instansnavn")] [InlineData("aos01:2712", "uden instansnavn")] [InlineData("", "Angiv")] public void Hosts(string host, string? error) { var result = ServerHost.Validate(host); if (error is null) { Assert.Null(result); } else { Assert.Contains(error, result, StringComparison.Ordinal); } } [Theory] [InlineData(@"tcp:SQL01.firma.local\PROD,1433", "sql01.firma.local", false)] [InlineData("sql02,1500", "sql02", false)] [InlineData(@".\SQLEXPRESS", "agent01", false)] [InlineData("(local)", "agent01", false)] [InlineData("localhost", "agent01", false)] [InlineData("(localdb)\\MSSQLLocalDB", null, true)] [InlineData("kunde.database.windows.net", null, true)] public void The_computer_behind_a_sql_data_source(string dataSource, string? host, bool skipped) { var (found, reason) = ServerHost.FromDataSource(dataSource, "AGENT01"); Assert.Equal(host, found); Assert.Equal(skipped, reason is not null); } [Fact] public void Lists_of_service_names() { Assert.Equal(["AOS60$01", "W3SVC"], ServerHost.ParseList("AOS60$01, W3SVC\nw3svc;")); Assert.Null(ServerHost.ValidateServiceNames(["AOS60$01", "MSSQL$PROD"])); Assert.NotNull(ServerHost.ValidateServiceNames([@"bad\name"])); } [Theory] [InlineData(@"NT SERVICE\SQLInsight Agent", "aos01.firma.local", @"FIRMA\AGENT01$")] [InlineData("LocalSystem", "aos01", @"FIRMA\AGENT01$")] [InlineData(@"NT SERVICE\SQLInsight Agent", "agent01.firma.local", @"NT SERVICE\SQLInsight Agent")] [InlineData(@"FIRMA\gmsa-sqlinsight$", "aos01", @"FIRMA\gmsa-sqlinsight$")] public void The_account_the_agent_arrives_as(string service, string host, string expected) => Assert.Equal(expected, ServerGrantScript.AccountFor(service, host, "AGENT01", "FIRMA")); [Theory] [InlineData(0u, false)] [InlineData(TaskResults.Running, false)] [InlineData(TaskResults.HasNotRun, false)] [InlineData(TaskResults.AlreadyRunning, false)] [InlineData(1u, true)] [InlineData(0x80070005u, true)] [InlineData(TaskResults.TerminatedByUser, true)] public void Task_results(uint result, bool failure) { Assert.Equal(failure, TaskResults.IsFailure(result)); Assert.False(string.IsNullOrWhiteSpace(TaskResults.Describe(result))); } }