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 required, Func evaluate) : IRule { public int Calls { get; private set; } public string Id => id; public string Title => $"Stub {id}"; public IReadOnlyCollection RequiredCapabilities => required; public RuleEvaluation Evaluate(RuleContext context) { Calls++; return evaluate(context); } } [Fact] public void Rule_needing_what_the_version_lacks_is_not_applicable_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("9.0.5057.0"))); Assert.Equal(RuleOutcome.NotApplicable, result.Outcome); Assert.Equal([Capability.QueryStore], result.MissingCapabilities); Assert.Equal("Ikke relevant for SQL Server 2005: kræver QueryStore (fra SQL Server 2016).", result.Reason); Assert.Equal(0, rule.Calls); } [Fact] public void Rule_needing_a_setting_the_instance_lacks_is_still_not_measurable() { // Always On not enabled is a setting, not the version: the rule could run, so it is a gap, not "not applicable". var rule = new StubRule("AG", [Capability.HadrEnabled], _ => RuleEvaluation.Passed()); var result = Assert.Single(new RuleEngine([rule]).Evaluate(TestInstances.Context("16.0.4135.4"))); Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome); Assert.Contains("Ikke målbar", result.Reason, StringComparison.Ordinal); } [Fact] public void Rule_needing_data_the_version_does_not_have_is_not_applicable() { var rule = new StubRule("D", [], ctx => { ctx.Data.Get(); return RuleEvaluation.Passed(); }); var context = TestInstances.Context("9.0.5057.0"); context.Data.MarkNotApplicable(typeof(SQLInsight.Core.Data.DeadlockSnapshot), "Ikke relevant for SQL Server 2005: kræver SystemHealthSession (fra SQL Server 2008)."); var result = Assert.Single(new RuleEngine([rule]).Evaluate(context)); Assert.Equal(RuleOutcome.NotApplicable, result.Outcome); Assert.Contains("SystemHealthSession", result.Reason, StringComparison.Ordinal); // Collected later (or failing for another reason): the flag does not stick. context.Data.MarkUnavailable("Collector 'deadlocks' fejlede: adgang nægtet."); Assert.Equal(RuleOutcome.NotMeasurable, Assert.Single(new RuleEngine([rule]).Evaluate(context)).Outcome); } [Theory] [InlineData(Capability.QueryStore, true)] [InlineData(Capability.SystemHealthSession, true)] [InlineData(Capability.TextQueryPlan, true)] [InlineData(Capability.QueryStatsMemoryColumns, true)] [InlineData(Capability.SelfManagedInfrastructure, true)] [InlineData(Capability.HadrEnabled, false)] public void Only_what_the_instance_is_makes_a_rule_not_applicable(Capability capability, bool inherent) => Assert.Equal(inherent, InstanceCapabilities.IsInherent(capability)); [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(); return RuleEvaluation.Passed(); }); var context = TestInstances.Context("16.0.4135.4"); context.Data.MarkUnavailable("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(() => new RuleEngine([a, b])); } [Fact] public void Found_requires_at_least_one_finding() { Assert.Throws(() => 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"))); } }