using SQLInsight.Core.Collection;
using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Rules.Engine;
using SQLInsight.Rules.Performance;
using static SQLInsight.Rules.Tests.TestInstances;
namespace SQLInsight.Rules.Tests;
public class OperationRuleTests
{
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
private static LongOperation Backup(DateTimeOffset started, DateTimeOffset seen, string db = "AX") =>
new(61, "BACKUP DATABASE", db, @"KUNDE\svc-backup", started, 40, seen.AddMinutes(30), seen);
/// Earlier runs of 20, 22, 25, 21 and 60 minutes (median 22).
private static LongOperationSnapshot WithHistory()
{
LongOperationSnapshot? snapshot = null;
var day = Now.AddDays(-10);
foreach (var minutes in new[] { 20, 22, 25, 21, 60 })
{
snapshot = LongOperationHistory.Advance(snapshot, [Backup(day, day.AddMinutes(minutes))], day.AddMinutes(minutes));
snapshot = LongOperationHistory.Advance(snapshot, [], day.AddMinutes(minutes + 1));
day = day.AddDays(1);
}
return snapshot!;
}
[Fact]
public void History_keeps_finished_runs_per_command_and_database()
{
var snapshot = WithHistory();
Assert.Equal(5, snapshot.History["BACKUP DATABASE|AX"].Count);
Assert.Equal(TimeSpan.FromMinutes(22), LongOperationHistory.Median(snapshot, Backup(Now, Now)));
Assert.Null(LongOperationHistory.Median(snapshot, Backup(Now, Now, "Andet")));
}
[Theory]
[InlineData(40, RuleOutcome.Passed)] // under 2 × 22
[InlineData(50, RuleOutcome.Findings)]
public void Backups_running_more_than_twice_the_median_are_found(int minutes, RuleOutcome expected)
{
var running = WithHistory() with { CapturedAt = Now, Running = [Backup(Now.AddMinutes(-minutes), Now)] };
var result = Run(new SlowBackupRule(), With(running));
Assert.Equal(expected, result.Outcome);
if (expected == RuleOutcome.Findings)
{
var finding = Assert.Single(result.Findings);
Assert.Equal(Severity.Medium, finding.Severity);
Assert.True(finding.Evidence.Single(e => e.Name == "Login").Sensitive);
}
}
[Fact]
public void Other_operations_and_unknown_history_are_not_judged()
{
var dbcc = new LongOperation(62, "DBCC", "AX", null, Now.AddHours(-5), 10, null, Now);
var snapshot = new LongOperationSnapshot { CapturedAt = Now, Running = [dbcc, Backup(Now.AddHours(-5), Now, "Ny")] };
Assert.Equal(RuleOutcome.Passed, Run(new SlowBackupRule(), With(snapshot)).Outcome);
}
private static CumulativeSample Queries(double threshold, params QueryStat[] queries) => new(
new QueryStatsSnapshot { CapturedAt = Now, ServerTime = Now.UtcDateTime, SqlServerStartTime = Now.UtcDateTime.AddDays(-3), Queries = queries, ActivityThresholdPercent = threshold },
null);
private static QueryStat Statement(string key, double hours, double cpuSeconds) => new()
{
Key = key, Executions = 1000, WorkerTimeUs = (long)(cpuSeconds * 1_000_000), ElapsedTimeUs = 0, LogicalReads = 0, PhysicalReads = 0, LogicalWrites = 0,
CreatedAt = Now.UtcDateTime.AddHours(-hours), LastExecutionAt = Now.UtcDateTime.AddMinutes(-1), Text = "SELECT * FROM INVENTSUM",
};
[Fact]
public void A_statement_using_more_than_half_a_core_for_hours_is_found()
{
var busy = Statement("0xBUSY", 4, 4 * 3600 * 0.8); // 80 %
var calm = Statement("0xCALM", 4, 4 * 3600 * 0.2); // 20 %
var young = Statement("0xNEW", 0.5, 1800); // 100 %, but only half an hour
var result = Run(new ConstantActivityRule(), With(Queries(50, busy, calm, young)));
var finding = Assert.Single(result.Findings);
Assert.Equal("query: 0xBUSY", finding.Subject);
Assert.DoesNotContain("INVENTSUM", finding.Title, StringComparison.Ordinal);
Assert.True(finding.Evidence.Single(e => e.Name == "Sætning").Sensitive);
Assert.Equal(RuleOutcome.Passed, Run(new ConstantActivityRule(), With(Queries(90, busy))).Outcome); // configurable
Assert.InRange(busy.ActivityPercent!.Value, 79, 82);
}
}