using SQLInsight.Core.Data; using SQLInsight.Reports; using SQLInsight.Repository; namespace SQLInsight.Agent.Tests; public class TimeWindowTests { private static readonly TimeZoneInfo Copenhagen = TimeZoneInfo.FindSystemTimeZoneById("Europe/Copenhagen"); private static readonly DateTimeOffset Now = new(2026, 10, 1, 10, 0, 0, TimeSpan.Zero); [Fact] public void Local_input_is_converted_to_utc_in_the_display_time_zone() { // 20 September 2026 is summer time in Copenhagen (UTC+2): 03:00–04:00 local is 01:00–02:00 UTC. Assert.True(TimeWindow.TryParse("2026-09-20T03:00", "2026-09-20T04:00", Copenhagen, out var from, out var to, out var error)); Assert.Null(error); Assert.Equal(new DateTimeOffset(2026, 9, 20, 1, 0, 0, TimeSpan.Zero), from); Assert.Equal(new DateTimeOffset(2026, 9, 20, 2, 0, 0, TimeSpan.Zero), to); Assert.Equal("2026-09-20T03:00", TimeWindow.ToInput(from, Copenhagen)); } [Fact] public void Winter_time_and_the_skipped_summer_time_hour_are_handled() { Assert.True(TimeWindow.TryParse("2026-12-01T03:00", "2026-12-01T04:00", Copenhagen, out var from, out _, out _)); Assert.Equal(new DateTimeOffset(2026, 12, 1, 2, 0, 0, TimeSpan.Zero), from); // 29 March 2026 02:30 does not exist in Copenhagen: the hour after is used rather than failing. Assert.True(TimeWindow.TryParse("2026-03-29T02:30", "2026-03-29T05:00", Copenhagen, out var skipped, out _, out _)); Assert.Equal(new DateTimeOffset(2026, 3, 29, 1, 30, 0, TimeSpan.Zero), skipped); } [Theory] [InlineData("2026-09-20T04:00", "2026-09-20T03:00", "Til skal ligge efter fra.")] [InlineData("2026-08-01T00:00", "2026-09-20T03:00", "Højst 31 dage ad gangen.")] [InlineData("i går", "2026-09-20T03:00", "Angiv fra og til som dato og klokkeslæt.")] public void Invalid_windows_are_refused_with_a_reason(string from, string to, string expected) { Assert.False(TimeWindow.TryParse(from, to, Copenhagen, out _, out _, out var error)); Assert.Equal(expected, error); } [Fact] public void Raw_samples_are_used_inside_their_retention_and_hourly_history_before() { Assert.Equal(WindowResolution.Raw, TimeWindow.ChooseResolution(Now.AddDays(-13), Now, rawDays: 14)); Assert.Equal(WindowResolution.Hourly, TimeWindow.ChooseResolution(Now.AddDays(-15), Now, rawDays: 14)); } [Fact] public void Presets_follow_the_local_calendar() { // 10:00 UTC = 12:00 in Copenhagen. var (nightFrom, nightTo) = TimeWindow.Preset("i-nat", Now, Copenhagen)!.Value; Assert.Equal(new DateTimeOffset(2026, 9, 30, 22, 0, 0, TimeSpan.Zero), nightFrom); Assert.Equal(new DateTimeOffset(2026, 10, 1, 4, 0, 0, TimeSpan.Zero), nightTo); var (yesterdayFrom, yesterdayTo) = TimeWindow.Preset("i-gaar", Now, Copenhagen)!.Value; Assert.Equal(new DateTimeOffset(2026, 9, 29, 22, 0, 0, TimeSpan.Zero), yesterdayFrom); Assert.Equal(new DateTimeOffset(2026, 9, 30, 22, 0, 0, TimeSpan.Zero), yesterdayTo); // Before 06 local, "i nat" ends now. var early = new DateTimeOffset(2026, 10, 1, 2, 0, 0, TimeSpan.Zero); Assert.Equal(early, TimeWindow.Preset("i-nat", early, Copenhagen)!.Value.To); Assert.Null(TimeWindow.Preset("ukendt", Now, Copenhagen)); } [Fact] public void Waits_are_compared_per_minute_with_the_weeks_that_have_history() { var window = new List { new("LCK_M_X", 50, 600_000, 0), // 10 min in a 60-minute window = 10 000 ms/min new("PAGEIOLATCH_SH", 1_000, 60_000, 0), // 1 min = 1 000 ms/min }; IReadOnlyList> weeks = [ [new("LCK_M_X", 60_000, 10, 1), new("PAGEIOLATCH_SH", 60_000, 900, 1)], // 1 000 ms/min each [new("LCK_M_X", 180_000, 10, 1), new("PAGEIOLATCH_SH", 60_000, 900, 1)], // 3 000 and 1 000 [], // no data that week: ignored ]; var result = WindowBaseline.CompareWaits(window, 60, weeks); var locks = result[0]; Assert.Equal("LCK_M_X", locks.WaitType); Assert.Equal(10_000, locks.PerMinute); Assert.Equal(2_000, locks.NormalPerMinute); Assert.Equal(5, locks.Ratio); Assert.True(locks.IsUnusual); var io = result[1]; Assert.Equal(1_000, io.NormalPerMinute); Assert.False(io.IsUnusual); } [Fact] public void Without_history_the_normal_is_unknown_and_nothing_is_flagged_by_ratio() { var result = WindowBaseline.CompareWaits([new WaitTotal("CXPACKET", 1, 1_000, 0)], 60, [[], []]); Assert.Null(result[0].NormalPerMinute); Assert.Null(result[0].Ratio); Assert.False(result[0].IsUnusual); } [Fact] public void Metric_is_unusual_when_clearly_worse_than_normal() { var at = Now.UtcDateTime; var cpu = new MetricComparison("cpu", "CPU", " %", 0, LowerIsWorse: false, 100, [new MetricPoint(at, 80), new MetricPoint(at.AddMinutes(5), 90)], WindowBaseline.NormalAverage([[new MetricPoint(at.AddDays(-7), 30)], [new MetricPoint(at.AddDays(-14), 50)]])); Assert.Equal(40, cpu.NormalAverage); Assert.True(cpu.IsUnusual); var ple = new MetricComparison("ple", "PLE", "", 0, LowerIsWorse: true, null, [new MetricPoint(at, 200)], 1_000); Assert.True(ple.IsUnusual); var normal = new MetricComparison("cpu", "CPU", " %", 0, false, 100, [new MetricPoint(at, 42)], 40); Assert.False(normal.IsUnusual); Assert.Null(WindowBaseline.NormalAverage([[], []])); } [Theory] [InlineData("varighed", TopQueryOrder.Duration)] [InlineData("laesninger", TopQueryOrder.Reads)] [InlineData("koersler", TopQueryOrder.Executions)] [InlineData(null, TopQueryOrder.Cpu)] [InlineData("noget-andet", TopQueryOrder.Cpu)] public void Order_comes_from_the_url(string? value, TopQueryOrder expected) => Assert.Equal(expected, AnalysisOrder.Parse(value)); }