Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
Collectors (SQL Server 2000+, version-specific SQL, least privilege): server, configuration, trace flags, databases (settings, backups in UTC, CHECKDB, VLFs, Query Store, files, tempdb), suspect pages, plan cache, wait stats and file I/O as deltas with restart/clear detection, performance counters, CPU from the scheduler monitor ring buffer, blocking (sleeping head blockers with open transactions), and deadlocks from system_health continuing from the last file offset. Rules: configuration (memory, MAXDOP per Microsoft NUMA guidance, cost threshold, ad hoc bloat, risky options, pending changes), databases (state, settings, backups, log backups, CHECKDB, suspect pages, VLFs, autogrowth, max size, Query Store, tempdb), waits (THREADPOOL, signal waits, locks, I/O, WRITELOG, parallelism, PAGELATCH), I/O latency, CPU, memory grants, blocking and deadlocks. Missing data makes a rule not measurable with the collector's reason. Integration tests run every collector as the least-privilege login and reproduce real blocking and a real deadlock; green on 2017 and 2022. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
111 lines
3.8 KiB
C#
111 lines
3.8 KiB
C#
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.Rules.Tests;
|
|
|
|
public class RuleEngineTests
|
|
{
|
|
private sealed class StubRule(string id, IReadOnlyCollection<Capability> required, Func<RuleContext, RuleEvaluation> evaluate) : IRule
|
|
{
|
|
public int Calls { get; private set; }
|
|
public string Id => id;
|
|
public string Title => $"Stub {id}";
|
|
public IReadOnlyCollection<Capability> RequiredCapabilities => required;
|
|
|
|
public RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
Calls++;
|
|
return evaluate(context);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Rule_missing_capabilities_is_not_measurable_and_never_runs()
|
|
{
|
|
var rule = new StubRule("QS", [Capability.QueryStore], _ => RuleEvaluation.Passed());
|
|
var engine = new RuleEngine([rule]);
|
|
|
|
var result = Assert.Single(engine.Evaluate(TestInstances.Context("11.0.7001.0")));
|
|
|
|
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
|
|
Assert.Equal([Capability.QueryStore], result.MissingCapabilities);
|
|
Assert.Contains("SQL Server 2012", result.Reason, StringComparison.Ordinal);
|
|
Assert.Equal(0, rule.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rule_with_capabilities_runs()
|
|
{
|
|
var rule = new StubRule("QS", [Capability.QueryStore], _ => RuleEvaluation.Passed());
|
|
var engine = new RuleEngine([rule]);
|
|
|
|
var result = Assert.Single(engine.Evaluate(TestInstances.Context("16.0.4135.4")));
|
|
|
|
Assert.Equal(RuleOutcome.Passed, result.Outcome);
|
|
Assert.Equal(1, rule.Calls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Throwing_rule_is_reported_as_error_and_does_not_stop_others()
|
|
{
|
|
var failing = new StubRule("A", [], _ => throw new InvalidOperationException("boom"));
|
|
var finding = new Finding
|
|
{
|
|
RuleId = "B",
|
|
Severity = Severity.Low,
|
|
Title = "t",
|
|
Explanation = "e",
|
|
Recommendation = "r",
|
|
};
|
|
var working = new StubRule("B", [], _ => RuleEvaluation.Found(finding));
|
|
var engine = new RuleEngine([failing, working]);
|
|
|
|
var results = engine.Evaluate(TestInstances.Context("16.0.4135.4"));
|
|
|
|
Assert.Equal(RuleOutcome.Error, results[0].Outcome);
|
|
Assert.Contains("boom", results[0].Reason, StringComparison.Ordinal);
|
|
Assert.Equal(RuleOutcome.Findings, results[1].Outcome);
|
|
Assert.Same(finding, Assert.Single(results[1].Findings));
|
|
}
|
|
|
|
[Fact]
|
|
public void Rule_needing_uncollected_data_is_not_measurable_with_the_collectors_reason()
|
|
{
|
|
var rule = new StubRule("W", [], ctx =>
|
|
{
|
|
ctx.Data.Get<SQLInsight.Core.Data.WaitStatsSnapshot>();
|
|
return RuleEvaluation.Passed();
|
|
});
|
|
var context = TestInstances.Context("16.0.4135.4");
|
|
context.Data.MarkUnavailable<SQLInsight.Core.Data.WaitStatsSnapshot>("Adgang nægtet til sys.dm_os_wait_stats.");
|
|
|
|
var result = Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
|
|
|
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
|
|
Assert.Equal("Adgang nægtet til sys.dm_os_wait_stats.", result.Reason);
|
|
}
|
|
|
|
[Fact]
|
|
public void Duplicate_rule_ids_are_rejected()
|
|
{
|
|
var a = new StubRule("SAME", [], _ => RuleEvaluation.Passed());
|
|
var b = new StubRule("same", [], _ => RuleEvaluation.Passed());
|
|
|
|
Assert.Throws<ArgumentException>(() => new RuleEngine([a, b]));
|
|
}
|
|
|
|
[Fact]
|
|
public void Found_requires_at_least_one_finding()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => RuleEvaluation.Found());
|
|
}
|
|
|
|
[Fact]
|
|
public void Built_in_rule_ids_are_unique()
|
|
{
|
|
var engine = new RuleEngine(BuiltInRules.Create());
|
|
|
|
Assert.NotEmpty(engine.Evaluate(TestInstances.Context("16.0.4135.4")));
|
|
}
|
|
}
|